DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock
Содержание
Концепция
Продолжаем работу над функционалом элемента управления WinForms "Панель", и сегодня рассмотрим такие её свойства как Padding и Dock.
WinForm-объект "Панель" по сути является обычным контейнером для размещения внутри него других WinForm-объектов. При размещении таких объектов мы можем самостоятельно указать требуемые координаты размещения объекта, и он будет расположен на указанных координатах. Но мы можем также указать и способ привязки размещаемого в контейнере объекта после его создания внутри контейнера. И для этого существует шесть способов привязки объекта внутри своего контейнера (свойство Dock объекта):
- Присоединение к верхней границе и растягивание на ширину контейнера,
- Присоединение к нижней границе и растягивание на ширину контейнера,
- Присоединение к левой границе и растягивание на высоту контейнера,
- Присоединение к правой границе и растягивание на высоту контейнера,
- Растягивание на ширину и высоту всего контейнера (заполнение),
- Объект прикреплён к указанным координатам и его размеры не меняются.
При этом, если выбран один из способов привязки, где объект прилипает к одной, или всем границам своего контейнера, его края притягиваются к границе контейнера с учётом величины Padding, установленной для контейнера — граница располагаемого объекта будет притянута не к границе контейнера, а будет отстоять от его границы на дистанцию, величина которой указана в значении Padding контейнера.
Способ же расположения объекта внутри контейнера указывается в его значении Dock. При этом, если в контейнере находится не один объект, то каждый последующий будет "прилипать" не к границе контейнера на дистанции Padding, а к предыдущему объекту, притянутому к той же стороне контейнера.
На изображении показано как в MS Visual Studio притягиваются объекты к верхней грани своего контейнера, для которого установлено значение Padding равное 20, если для них выбрать притягивание к верхней границе контейнера:
Сегодня сделаем все возможные варианты расположения объекта внутри своего контейнера только для одного объекта.
Для того, чтобы учитывать значение Padding контейнера, мы сделаем не смещение координат расположения объекта внутри панели, а в самом объекте "Панель" добавим ещё один объект на канвасе — объект-подложку, и уже на него будем располагать все нужные объекты внутри панели.
Зачем так? Просто это будет удобнее для расположения объектов с учётом значения Padding контейнера — сам объект-подложка будет смещён на величину этого свойства, плюс, когда нам потребуется рисовать что-либо на канвасе, то рисовать это будем как раз на подложке — она уже будет смещена на значение Padding, и не потребуется рассчитывать координаты и размеры рисунка на этом объекте. И ещё другие дополнительные удобства, которые впоследствии сможем оценить при разработке других WinForm-объектов.
Доработка классов библиотеки
У нас есть возможность создания различных стилей объектов-форм, и для их определения существует отдельный файл в библиотеке, где мы постепенно прописываем дополнительные параметры для стилей. Так как объект-форма у нас может закрашиваться не только одним цветом, но и градиентной заливкой, то укажем в файле \MQL5\Include\DoEasy\GraphINI.mqh новые параметры стилей для градиентной заливки фона объекта-формы:
//+------------------------------------------------------------------+ //| Список индексов параметров стилей форм | //+------------------------------------------------------------------+ enum ENUM_FORM_STYLE_PARAMS { //--- CForm FORM_STYLE_FRAME_SHADOW_OPACITY, // Непрозрачность тени FORM_STYLE_FRAME_SHADOW_BLUR, // Размытие тени FORM_STYLE_DARKENING_COLOR_FOR_SHADOW, // Затемнённость цвета тени формы FORM_STYLE_FRAME_SHADOW_X_SHIFT, // Смещение тени по оси X FORM_STYLE_FRAME_SHADOW_Y_SHIFT, // Смещение тени по оси Y FORM_STYLE_GRADIENT_V, // Флаг вертикальной градиентной заливки FORM_STYLE_GRADIENT_C, // Флаг циклической градиентной заливки //--- CPanel FORM_STYLE_FRAME_WIDTH_LEFT, // Ширина рамки панели слева FORM_STYLE_FRAME_WIDTH_RIGHT, // Ширина рамки панели справа FORM_STYLE_FRAME_WIDTH_TOP, // Ширина рамки панели сверху FORM_STYLE_FRAME_WIDTH_BOTTOM, // Ширина рамки панели снизу }; #define TOTAL_FORM_STYLE_PARAMS (11) // Количество параметров стиля формы //+------------------------------------------------------------------+ //| Массив, содержащий параметры стилей форм | //+------------------------------------------------------------------+ int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]= { //--- Параметры стиля формы "Плоская форма" { //--- CForm 80, // Непрозрачность тени 4, // Размытие тени 80, // Затемнённость цвета тени формы 2, // Смещение тени по оси X 2, // Смещение тени по оси Y false, // Флаг вертикальной градиентной заливки false, // Флаг циклической градиентной заливки //--- CPanel 3, // Ширина рамки панели слева 3, // Ширина рамки панели справа 3, // Ширина рамки панели сверху 3, // Ширина рамки панели снизу }, //--- Параметры стиля формы "Рельефная форма" { //--- CForm 80, // Непрозрачность тени 4, // Размытие тени 80, // Затемнённость цвета тени формы 2, // Смещение тени по оси X 2, // Смещение тени по оси Y true, // Флаг вертикальной градиентной заливки false, // Флаг циклической градиентной заливки //--- CPanel 3, // Ширина рамки панели слева 3, // Ширина рамки панели справа 3, // Ширина рамки панели сверху 3, // Ширина рамки панели снизу }, }; //+------------------------------------------------------------------+
Количество параметров стилей увеличим с 9 до 11.
В файле \MQL5\Include\DoEasy\Defines.mqh в перечислении границ элемента управления, привязанных к контейнеру, переместим константу, указывающую на отсутствие привязки объекта к границам контейнера, на первое место:
//+------------------------------------------------------------------+ //| Границы элемента управления, привязанные к контейнеру | //+------------------------------------------------------------------+ enum ENUM_CANV_ELEMENT_DOCK_MODE { CANV_ELEMENT_DOCK_MODE_NONE, // Прикреплён к указанным координатам, размеры не меняются CANV_ELEMENT_DOCK_MODE_TOP, // Присоединение сверху и растягивание на ширину контейнера CANV_ELEMENT_DOCK_MODE_BOTTOM, // Присоединение снизу и растягивание на ширину контейнера CANV_ELEMENT_DOCK_MODE_LEFT, // Присоединение слева и растягивание на высоту контейнера CANV_ELEMENT_DOCK_MODE_RIGHT, // Присоединение справа и растягивание на высоту контейнера CANV_ELEMENT_DOCK_MODE_FILL, // Растягивание на ширину и высоту всего контейнера }; //+------------------------------------------------------------------+
Ранее константа была на последнем месте в списке. Это не очень удобно в случае, если мы хотим проверить, что объект не привязан к границам контейнера. Теперь же мы можем просто проверить на ложь значение метода, возвращающего тип привязки объекта к контейнеру, так как значение 0, которому соответствует теперь эта константа, в булевом эквиваленте равно false.
В файле E:\MetaQuotes\MetaTrader 5\MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:
MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE, // Не удалось изменить размер массива цветов MSG_LIB_SYS_FAILED_ARRAY_RESIZE, // Не удалось изменить размер массива MSG_LIB_SYS_FAILED_ARRAY_COPY, // Не удалось скопировать массив MSG_LIB_SYS_FAILED_ADD_BUFFER, // Не удалось добавить объект-буфер в список MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ, // Не удалось создать объект \"Индикаторный буфер\"
...
//--- CGCnvElement MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY, // Ошибка! Пустой массив MSG_CANV_ELEMENT_ERR_ARRAYS_NOT_MATCH, // Ошибка! Массив-копия ресурса не совпадает с оригиналом MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH, // Ошибка! Не удалось установить ширину канваса MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT, // Ошибка! Не удалось установить высоту канваса
...
//--- CGStdGraphObjExtToolkit MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA, // Не удалось изменить размер массива данных времени опорной точки MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA, // Не удалось изменить размер массива данных цены опорной точки MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM, // Не удалось создать объект-форму для контроля опорной точки //--- CPanel MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ, // Не удалось создать объект-подложку }; //+------------------------------------------------------------------+
и текстовые сообщения, соответствующие вновь добавленным индексам:
{"Не удалось изменить размер массива цветов","Failed to resize color array"}, {"Не удалось изменить размер массива ","Failed to resize array "}, {"Не удалось скопировать массив","Failed to copy array"}, {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"}, {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},
...
//--- CGCnvElement {"Ошибка! Пустой массив","Error! Empty array"}, {"Ошибка! Массив-копия ресурса не совпадает с оригиналом","Error! Array-copy of the resource does not match the original"}, {"Ошибка! Не удалось установить ширину канваса","Error! Failed to set canvas width"}, {"Ошибка! Не удалось установить высоту канваса","Error! Failed to set canvas height"},
...
//--- CGStdGraphObjExtToolkit {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"}, {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"}, {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"}, //--- CPanel {"Не удалось создать объект-подложку","Failed to create underlay object"}, }; //+---------------------------------------------------------------------+
При изменении размеров объекта-формы и его наследников, нам нужно будет полностью перерисовать объект. Для того, чтобы далее восстановить его исходные размеры, нам нужно будет знать его изначальные координаты и размеры. Кроме того, так как форма может быть закрашена как одним цветом, так и градиентной заливкой, то удобнее иметь в составе объекта массив, в котором указаны все цвета градиентной заливки. Если цвет один, то в массив он и записывается. Таким образом, мы всегда будем иметь в свойствах объекта только начальный цвет, а в массиве — все цвета, используемые для градиента. Соответственно, нам нужно будет доработать метод для установки одного цвета и добавить ещё один метод для установки цветов градиента. При этом оба метода будут заполнять значения как для одного цвета, так и для градиента.
Кроме того, разные объекты могут быть присоединены друг к другу, например, в одном объекте-панели есть два присоединённых WinForm-объекта. И этот объект-панель присоединён к ещё одной панели. Для двух объектов, присоединённых к первой панели, она будет являться базовым объектом, и будет прописана в их свойствах как базовый объект, который для них является контейнером. А вот когда мы этот контейнер с двумя объектами размещаем внутри ещё одного контейнера, то для первого контейнера новый объект-панель станет базовым контейнером. А чтобы два объекта, размещённые в первом контейнере могли знать какой объект является основным в этой иерархии, нам нужно добавить ещё одно свойство — главный объект всей иерархии. Тогда два объекта, расположенные в первом контейнере будут знать свой базовый контейнер, а также и главный объект — второй контейнер, к которому прикреплён их базовый.
Доработаем класс объекта-графического элемента в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
В его защищённой секции объявим новые переменные:
//+------------------------------------------------------------------+ //| Класс объекта графического элемента | //+------------------------------------------------------------------+ class CGCnvElement : public CGBaseObj { protected: CGCnvElement *m_element_main; // Указатель на первоначальный родительский элемент в составе всех групп связанных объектов CGCnvElement *m_element_base; // Указатель на родительский элемент в составе связанных объектов текущей группы CCanvas m_canvas; // Объект класса CCanvas CPause m_pause; // Объект класса "Пауза" bool m_shadow; // Наличие тени color m_chart_color_bg; // Цвет фона графика uint m_duplicate_res[]; // Массив для хранения копии данных ресурса color m_array_colors_bg[]; // Массив цветов фона элемента bool m_gradient_v; // Флаг вертикальной градиентной заливки bool m_gradient_c; // Флаг циклической градиентной заливки int m_init_relative_x; // Первоначальная относительная координата X int m_init_relative_y; // Первоначальная относительная координата Y //--- Создаёт (1) структуру объекта, (2) объект из структуры virtual bool ObjectToStruct(void); virtual void StructToObject(void); private:
В приватной секции класса объявим метод для сохранения массива цветов градиентной заливки:
//--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство ордера int IndexProp(ENUM_CANV_ELEMENT_PROP_DOUBLE property) const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL; } int IndexProp(ENUM_CANV_ELEMENT_PROP_STRING property) const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_DOUBLE_TOTAL; } //--- Сохраняет цвета в массив цветов фона void SaveColorsBG(color &colors[]); public:
В публичной секции класса напишем методы, устанавливающие и возвращающие значения вновь добавленных переменных:
//--- Создаёт элемент bool Create(const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool redraw=false); //--- (1) Устанавливает, (2) возвращает изначальное смещение координаты (1) X, (2) Y относительно базового объекта void SetCoordXRelativeInit(const int value) { this.m_init_relative_x=value; } void SetCoordYRelativeInit(const int value) { this.m_init_relative_y=value; } int CoordXRelativeInit(void) const { return this.m_init_relative_x; } int CoordYRelativeInit(void) const { return this.m_init_relative_y; } //--- (1) Устанавливает, (2) возвращает указатель на родительский элемент в составе связанных объектов текущей группы void SetBase(CGCnvElement *element) { this.m_element_base=element; } CGCnvElement *GetBase(void) { return this.m_element_base; } //--- (1) Устанавливает, (2) возвращает указатель на родительский элемент в составе всех групп связанных объектов void SetMain(CGCnvElement *element) { this.m_element_main=element; } CGCnvElement *GetMain(void) { return this.m_element_main; } //--- Возвращает указатель на объект-канвас CCanvas *GetCanvasObj(void) { return &this.m_canvas; }
В конструкторе по умолчанию инициализируем значением NULL указатель на главный объект иерархии связанных объектов:
//--- Конструктор по умолчанию/Деструктор CGCnvElement() : m_shadow(false),m_chart_color_bg((color)::ChartGetInteger(::ChartID(),CHART_COLOR_BACKGROUND)) { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_shift_coord_x=0; this.m_shift_coord_y=0; } ~CGCnvElement() { this.m_canvas.Destroy(); }
Доработаем метод, устанавливающий цвет фона графического элемента:
void SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift); void SetColorBackground(const color colour) { this.m_color_bg=colour; color arr[1]; arr[0]=colour; this.SaveColorsBG(arr); }
Теперь здесь не только записывается в свойства объекта переданное в метод значение цвета фона, но и вызывается метод, записывающий в новый массив цветов градиентной заливки этот единственный цвет. Для этого объявляем массив, записываем в него знечение цвета и передаём этот массив в метод SaveColorsBG(), который рассмотрим чуть позже.
Чуть ниже напишем метод, устанавливающий цвета градиентной заливки фона графического элемента:
void SetOpacity(const uchar value,const bool redraw=false); void SetColorsBackground(color &colors[]) { this.SaveColorsBG(colors); this.m_color_bg=this.m_array_colors_bg[0]; }
В метод передаётся массив цветов градиентной заливки, которые мы передаём в метод SaveColorsBG(), а затем в значение цвета фона записываем самый первый цвет из массива цветов. Таким образом мы установим сразу несколько разных цветов: если для фона графического элемента используется только один цвет, то это будет первый цвет из массива, если же используется градиентная заливка, то все цвета, переданные в метод в массиве, записываются в массив цветов градиентной заливки в методе SaveColorsBG().
Чуть ниже напишем ещё два метода — метод, возвращающий количество цветов градиентной заливки, и метод, возвращающий цвет из массива цветов по указанному индексу:
//--- Возвращает количество цветов, установленных для градиентной заливки фона uint ColorsBackgroundTotal(void) const { return this.m_array_colors_bg.Size(); } //--- Возвращает (1) цвет фона, (2) непрозрачность, координату (3) правого, (4) нижнего края элемента color ColorBackground(void) const { return this.m_color_bg; } color ColorBackground(const uint index) const { uint total=this.m_array_colors_bg.Size(); if(total==0) return this.m_color_bg; return(index>total-1 ? this.m_array_colors_bg[total-1] : this.m_array_colors_bg[index]); } uchar Opacity(void) const { return this.m_opacity; }
Первый метод просто возвращает размер массива цветов объекта.
Во втором методе, если размер массива цветов объекта равен нулю, то возвращается значение из переменной m_color_bg, иначе — если передан некорректный индекс (больше размера массива), то возвращается самый последний цвет, иначе — цвет, расположенный в массиве по указанному индексу.
В параметрическом конструкторе инициализируем указатель на главный объект иерархии присоединённых объектов и запишем в массив цветов градиентной заливки фона объекта цвет из переменной m_color_bg:
//+------------------------------------------------------------------+ //| Параметрический конструктор | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int element_id, const int element_num, const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable=true, const bool activity=true, const bool redraw=false) : m_shadow(false) { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND); this.m_name=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name; this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id); this.m_subwindow=wnd_num; this.m_type_element=element_type; this.SetFont(DEF_FONT,DEF_FONT_SIZE); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.m_color_bg=colour; this.m_opacity=opacity; this.m_shift_coord_x=0; this.m_shift_coord_y=0; if(::ArrayResize(this.m_array_colors_bg,1)==1) this.m_array_colors_bg[0]=this.m_color_bg; if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw)) {
В защищённом конструкторе пропишем те же самые строки (здесь это повторно показывать не имеет смысла).
Из метода, обновляющего координаты объекта, удалим проверку на его неперемещаемость:
//+------------------------------------------------------------------+ //| Обновляет координаты элемента | //+------------------------------------------------------------------+ bool CGCnvElement::Move(const int x,const int y,const bool redraw=false) { //--- Если элемент не перемещаемый или неактивный - уходим if(!this.Movable()) return false; //--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- Если стоит флаг обновления - перерисовываем график. if(redraw) ::ChartRedraw(this.ChartID()); //--- Возвращаем true return true; } //+------------------------------------------------------------------+
То, что эта проверка стоит внутри именно этого метода, это весьма логично, но это и создаёт большие проблемы при перемещении объектов, связанных в одну общую иерархию. Если мы перемещаем родительский объект, в составе которого есть вложенные объекты, и некоторые из них неперемещаемые, то сильно усложняется отслеживание их флагов неперемещаемости по наличию внутри объекта указателя на родительский и на главный объекты.
В общем, пока решил, что проще непосредственно при попытке сдвинуть объект мышкой отследить неперемещаемость только того объекта, который хотим двигать, чем делать многочисленные проверки на запрет перемещения и игнорировать их для всех вложенных в иерархии объектов.
Доработаем методы, устанавливающие новую ширину и высоту:
//+------------------------------------------------------------------+ //| Устанавливает новую ширину | //+------------------------------------------------------------------+ bool CGCnvElement::SetWidth(const int width) { if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width) return true; if(!this.m_canvas.Resize(width,this.m_canvas.Height())) { CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH); return false; } this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width); return true; } //+------------------------------------------------------------------+ //| Устанавливает новую высоту | //+------------------------------------------------------------------+ bool CGCnvElement::SetHeight(const int height) { if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height) return true; if(!this.m_canvas.Resize(this.m_canvas.Width(),height)) { CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT); return false; } this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height); return true; } //+------------------------------------------------------------------+
Ранее оба метода сразу же возвращали результат изменения размера высоты или ширины канваса:
return this.m_canvas.Resize(width,this.m_canvas.Height());
Но это не изменяло значения, записываемые в свойства объекта.
Поэтому теперь мы сначала проверяем значение, записанное в свойстве объекта на равенство переданному в метод значению и, если значения равны, то и изменять нечего — возвращаем сразу true.
Далее, если не удалось изменить размер канваса — сообщаем об этом в журнал и возвращаем false.
Если всё успешно — записываем новое значение в свойства объекта и возвращаем true.
Когда мы вызываем метод Erase() класса CCanvas, мы тем самым заполняем форму указанным цветом и непрозрачностью. Таким образом, если при вызове этого метода указать отличный от установленного в переменной m_color_bg (или в массиве цветов) цвет, то форма будет окрашена этим самым цветом. При передаче в метод массива цветов, запомним эти цвета во внутреннем массиве в двух методах Erase() объекта-графического элемента:
//+------------------------------------------------------------------+ //| Очищает элемент с заполнением его цветом и непрозрачностью | //+------------------------------------------------------------------+ void CGCnvElement::Erase(const color colour,const uchar opacity,const bool redraw=false) { color arr[1]; arr[0]=colour; this.SaveColorsBG(arr); this.m_canvas.Erase(::ColorToARGB(colour,opacity)); this.Update(redraw); } //+------------------------------------------------------------------+
В этом методе, закрашивающем форму одним цветом, установим этот же цвет и в массив цветов градиентной заливки объекта. Тем самым мы можем, окрашивая форму иным цветом, отличным от изначального, менять этот изначальный цвет для объекта.
В другом методе дополнительно сохраним переданные в метод значения в переменные, хранящие тип градиентной заливки, а затем сохраним и массив цветов в массиве объекта:
//+------------------------------------------------------------------+ //| Очищает элемент заливкой градиентом | //+------------------------------------------------------------------+ void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false) { //--- Устанавливаем флаги вертикальной и циклической градиентной заливки this.m_gradient_v=vgradient; this.m_gradient_c=cycle; //--- Проверяем размер массива цветов int size=::ArraySize(colors); //--- ... //--- ... //--- Сохраняем массив цветов фона this.SaveColorsBG(colors); //--- Если указано - обновляем канвас this.Update(redraw); } //+------------------------------------------------------------------+
Флаги типов градиентной заливки, сохраняемые в этом методе, позволят нам пересоздать объект с теми же самыми значениями градиента, которые были перед тем, как форма была перерисована с изменёнными размерами.
Метод, сохраняющий цвета в массив цветов фона:
//+------------------------------------------------------------------+ //| Сохраняет цвета в массив цветов фона | //+------------------------------------------------------------------+ void CGCnvElement::SaveColorsBG(color &colors[]) { if(this.m_array_colors_bg.Size()!=colors.Size()) { ::ResetLastError(); if(::ArrayResize(this.m_array_colors_bg,colors.Size())!=colors.Size()) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE); CMessage::ToLog(::GetLastError(),true); return; } } ::ArrayCopy(this.m_array_colors_bg,colors); } //+------------------------------------------------------------------+
Здесь: если размер переданного в метод массива цветов не совпадает с размером массива цветов объекта — изменяем размер массива цветов градиентной заливки объекта, а затем копируем переданный в метод массив в массив цветов объекта.
Так как у нас теперь есть методы, сохраняющие и возвращающие смещение в пикселях координат объекта относительно другого объекта, то в файле класса объекта-тени \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh удалим переменные, хранящие значения смещения тени относительно объекта, отбрасывающего тень, заменим на эти методы.
Из приватной секции класса удалим переменные:
//+------------------------------------------------------------------+ //| Класс объекта Тени | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { private: color m_color_shadow; // Цвет тени uchar m_opacity_shadow; // Непрозрачность тени int m_offset_x; // Смещение тени по оси X int m_offset_y; // Смещение тени по оси Y //--- Размытие по-Гауссу bool GaussianBlur(const uint radius);
Из публичной секции класса удалим методы, возвращающие значения удалённых переменных:
public: //--- Конструктор CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); //--- Поддерживаемые свойства объекта (1) целочисленные, (2) строковые virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Возвращает смещение тени по оси (1) X, (2) Y int OffsetX(void) const { return this.m_offset_x; } int OffsetY(void) const { return this.m_offset_y; } //--- Рисует тень объекта void DrawShadow(const int shift_x,const int shift_y,const uchar blur_value);
В конструкторе класса удалим инициализацию этих переменных:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CShadowObj::CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h) { this.m_type=OBJECT_DE_TYPE_GSHADOW; CGCnvElement::SetColorBackground(clrNONE); CGCnvElement::SetOpacity(0); CGCnvElement::SetActive(false); this.m_opacity_shadow=127; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,-50); this.m_shadow=false; this.m_visible=true; this.m_offset_x=0; this.m_offset_y=0; CGCnvElement::Erase(); } //+------------------------------------------------------------------+
В методе, рисующем тень объекта, заменим строки, записывающие значения в переменные
this.m_offset_x=shift_x; this.m_offset_y=shift_y;
и строку, где используются записанные в эти переменные значения
CGCnvElement::Move(this.CoordX()+this.m_offset_x,this.CoordY()+this.m_offset_y);
на установку значений посредством вызова методов, и чтение значений смещения, устанавливаемого в родительском классе, вместо чтения значений теперь уже удалённых переменных:
//+------------------------------------------------------------------+ //| Рисует тень объекта | //+------------------------------------------------------------------+ void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value) { //--- Устанавливаем в переменные значения смещения тени по осям X и Y this.SetCoordXRelative(shift_x); this.SetCoordYRelative(shift_y); //--- Рассчитываем ширину и высоту рисуемого прямоугольника int w=this.Width()-OUTER_AREA_SIZE*2; int h=this.Height()-OUTER_AREA_SIZE*2; //--- Рисуем закрашенный прямоугольник с рассчитанными размерами this.DrawShadowFigureRect(w,h); //--- Рассчитываем радиус размытия, который не может быть больше четверти размера константы OUTER_AREA_SIZE int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value); //--- Если размыть фигуру не удалось - уходим из метода (ошибку в журнал выведет GaussianBlur()) if(!this.GaussianBlur(radius)) return; //--- Смещаем объект тени на указанные в аргументах метода смещения по X и Y и обновляем канвас CGCnvElement::Move(this.CoordX()+this.CoordXRelative(),this.CoordY()+this.CoordYRelative()); CGCnvElement::Update(); } //+------------------------------------------------------------------+
Теперь для каждого объекта, родителем которого является класс объекта-графического элемента, значения смещений его координат относительно базового объекта будут всегда доступны. По этой причине мы здесь и удалили эти ненужные теперь переменные.
Доработаем класс объекта-формы в файле \MQL5\Include\DoEasy\Objects\Graph\Form.mqh.
Так как WinForms-объекты унаследованы от этого класса, то нам нужно перенести список для размещения указателей на объекты, создваемые внутри формы, из приватной секции в защищённую. И там же объявим метод, обновляющий координаты привязанных объектов:
protected: CArrayObj m_list_tmp; // Список для размещения указателей int m_frame_width_left; // Ширина рамки формы слева int m_frame_width_right; // Ширина рамки формы справа int m_frame_width_top; // Ширина рамки формы сверху int m_frame_width_bottom; // Ширина рамки формы снизу //--- Инициализирует переменные void Initialize(void); void Deinitialize(void); //--- Создаёт объект для тени void CreateShadowObj(const color colour,const uchar opacity); //--- Возвращает имя зависимого объекта string CreateNameDependentObject(const string base_name) const { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name; } //--- Обновляет координаты привязанных объектов virtual bool MoveDependentObj(const int x,const int y,const bool redraw=false); public:
При создании нового объекта, привязанного к форме, нам нужно указать в новом объекте тот объект, к которому он привязан — его главный объект.
Для этого в метод создания нового объекта будем передавать указатель на тот объект, из которого он создаётся:
//--- Создаёт новый присоединённый элемент bool CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, CGCnvElement *main, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity);
В методе инициализации переменных инициализируем значениями по умолчанию флаги типа градиентной заливки фона формы:
//+------------------------------------------------------------------+ //| Инициализирует переменные | //+------------------------------------------------------------------+ void CForm::Initialize(void) { this.m_list_elements.Clear(); this.m_list_elements.Sort(); this.m_list_tmp.Clear(); this.m_list_tmp.Sort(); this.m_shadow_obj=NULL; this.m_shadow=false; this.m_frame_width_right=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_left=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_top=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_bottom=DEF_FRAME_WIDTH_SIZE; this.m_gradient_v=true; this.m_gradient_c=false; this.m_mouse_state_flags=0; this.m_offset_x=0; this.m_offset_y=0; CGCnvElement::SetInteraction(false); this.m_animations=new CAnimations(CGCnvElement::GetObject()); this.m_list_tmp.Add(m_animations); } //+------------------------------------------------------------------+
По умолчанию для градиентной заливки будет использоваться вертикальный нециклический градиент.
В методе, создающем новый графический объект, ранее мы забыли указать флаг перемещаемости. Укажем его:
//+------------------------------------------------------------------+ //| Создаёт новый графический объект | //+------------------------------------------------------------------+ CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int obj_num, const string obj_name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; //--- ... if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); element.SetMovable(movable); return element; } //+------------------------------------------------------------------+
И точно так же в методе, создающем новый присоединённый элемент, во-первых, впишем указатель на его главный родительский объект иерархии связанных объектов, и установим свойства, ранее упущенные нами:
//+------------------------------------------------------------------+ //| Создаёт новый присоединённый элемент | //+------------------------------------------------------------------+ bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, CGCnvElement *main, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity) { //--- Если тип создаваемого графического элемента меньше, чем "элемент" - сообщаем об этом и возвращаем false if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19)); return false; } //--- Задаём номер элемента в списке int num=this.m_list_elements.Total()+1; //--- Создаём имя графического элемента string ns=(::StringLen((string)num)<2 ? ::IntegerToString(num,2,'0') : (string)num); string name="Elm"+ns; //--- Получаем экранные координаты объекта относительно системы координат базового объекта int elm_x=x; int elm_y=y; this.GetCoords(elm_x,elm_y); //--- Создаём новый графический элемент CGCnvElement *obj=this.CreateNewGObject(element_type,num,name,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) return false; //--- и добавляем его в список привязанных графических элементов if(!this.AddNewElement(obj,elm_x,elm_y)) { delete obj; return false; } //--- Устанавливаем минимальные свойства привязанному графическому элементу obj.SetColorBackground(colour); obj.SetOpacity(opacity); obj.SetActive(activity); obj.SetMain(main); obj.SetBase(this.GetObject()); obj.SetID(this.ID()); obj.SetCoordXRelative(x); obj.SetCoordYRelative(y); obj.SetZorder(this.Zorder(),false); obj.SetCoordXRelativeInit(x); obj.SetCoordYRelativeInit(y); //--- Рисуем добавленный объект и возвращаем true obj.Erase(colour,opacity,true); return true; } //+------------------------------------------------------------------+
Так как мы сегодня добавили два новых значения в стили форм, то в методе, устанавливающем стиль формы, добавим установку этих значений, и там же будем вызывать другой метод Erase() — тот, который закрашивает форму градиентной заливкой. При этом, если в массиве цветов градиентной заливки всего один цвет, то форма будет окрашена этим одним цветом — без градиента:
//+------------------------------------------------------------------+ //| Устанавливает стиль формы | //+------------------------------------------------------------------+ void CForm::SetFormStyle(const ENUM_FORM_STYLE style, const ENUM_COLOR_THEMES theme, const uchar opacity, const bool shadow=false, const bool use_bg_color=true, const bool redraw=false) { //--- Устанавливаем параметры непрозрачности и размера сторон рамки формы this.m_shadow=shadow; this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP]; this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM]; this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT]; this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT]; this.m_gradient_v=array_form_style[style][FORM_STYLE_GRADIENT_V]; this.m_gradient_c=array_form_style[style][FORM_STYLE_GRADIENT_C]; //--- Создаём объект тени this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]); //--- Устанавливаем цветовую схему this.SetColorTheme(theme,opacity); //--- Рассчитываем цвет тени с затемнением цвета color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW])); this.SetColorShadow(color_shadow); //--- Рисуем прямоугольную тень int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT]; int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT]; this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]); //--- Заполняем фон формы цветом и непрозрачностью this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); //--- В зависимости от выбранного стиля формы рисуем соответствующую рамку формы и внешнюю очерчивающую рамку switch(style) { case FORM_STYLE_BEVEL : this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL); break; //---FORM_STYLE_FLAT default: this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT); break; } this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity()); } //+------------------------------------------------------------------+
В методе, обновляющем координаты объекта, мы ранее в цикле проходили по всем привязанным к форме объектам и смещали их координаты вслед за координатами текущего объекта. Теперь мы объявили новый метод MoveDependentObj(), в котором будет организован этот цикл.
Поэтому изменим и сам метод, обновляющий координаты элемента:
//+------------------------------------------------------------------+ //| Обновляет координаты элемента | //+------------------------------------------------------------------+ bool CForm::Move(const int x,const int y,const bool redraw=false) { CGCnvElement *base=this.GetBase(); CGCnvElement *main=this.GetMain(); bool res=true; //--- Если элемент не перемещаемый, и это базовый объект - уходим if(!this.Movable() && base==NULL) return false; //--- Если есть тень у объекта и не удалось записать новые значения координат в свойства объекта-тени - возвращаем false if(this.m_shadow) { if(this.m_shadow_obj==NULL || !this.m_shadow_obj.Move(x-OUTER_AREA_SIZE+this.m_shadow_obj.CoordXRelative(),y-OUTER_AREA_SIZE+this.m_shadow_obj.CoordYRelative(),false)) return false; } //--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- Смещаем все привязанные объекты if(!this.MoveDependentObj(x,y,false)) return false; //--- Если стоит флаг обновления, и это базовый объект - перерисовываем график. if(redraw && main==NULL) ::ChartRedraw(this.ChartID()); //--- Возвращаем true return true; } //+------------------------------------------------------------------+
В первую очередь, мы добавим получение указателя на главный объект всей иерархии связанных объектов — это именно тот объект, перемещение которого приводит к перемещению всех остальных, связанных с ним (и друг с другом) элементов.
При перемещении объекта-тени теперь используем методы объекта-графического элемента для получения смещения тени относительно объекта, отбрасывающаго эту тень.
Вместо цикла теперь вызываем новый метод MoveDependentObj(), который рассмотрим ниже.
В конце теперь проверяем, что это главный объект всей цепочки иерархии связанных объектов, а не базовый объект лишь одной из цепочек иерархии.
Метод, обновляющий координаты привязанных объектов:
//+------------------------------------------------------------------+ //| Обновляет координаты привязанных объектов | //+------------------------------------------------------------------+ bool CForm::MoveDependentObj(const int x,const int y,const bool redraw=false) { //--- В цикле по всем привязанным объектам for(int i=0;i<this.m_list_elements.Total();i++) { //--- получаем очередной объект и смещаем его CGCnvElement *obj=m_list_elements.At(i); if(obj==NULL) continue; if(!obj.Move(x+obj.CoordXRelative(),y+obj.CoordYRelative(),false)) return false; } return true; } //+------------------------------------------------------------------+
Здесь просто в цикле по всем привязанным объектам получаем очередной объект и вызываем метод Move() этого объекта. Соответственно, в том объекте точно так же будут вызваны все методы Move() для всех объектов, с ним связанных. Это вызовет перемещение всей иерархии связей всех объектов, прикреплённых к перемещаемому.
Теперь доработаем класс WinForms-объекта "Панель" в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh.
В приватной секции класса объявим указатель на объект-подложку, переменные для хранения начальных координат и размеров панели при её создании и методы для создания подложки и привязки элемента к контейнеру:
//+------------------------------------------------------------------+ //| Класс объекта Panel элементов управления WForms | //+------------------------------------------------------------------+ class CPanel : public CForm { private: CGCnvElement *m_underlay; // Подложка для размещения элементов color m_fore_color; // Цвет текста по умолчанию для всех объектов на панели ENUM_FW_TYPE m_bold_type; // Тип толщины шрифта ENUM_FRAME_STYLE m_border_style; // Стиль рамки панели bool m_autoscroll; // Флаг автоматического появления полосы прокрутки int m_autoscroll_margin[2]; // Массив полей вокруг элемента управления при автоматической прокрутке bool m_autosize; // Флаг автоматического изменения размера элемента под содержимое ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode; // Режим автоматического изменения размера элемента под содержимое ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode; // Режим привязки границ элемента к контейнеру int m_margin[4]; // Массив промежутков всех сторон между полями данного и другого элемента управления int m_padding[4]; // Массив промежутков всех сторон внутри элемента управления int m_init_x; // Координата X панели при её создании int m_init_y; // Координата Y панели при её создании int m_init_w; // Ширина панели при её создании int m_init_h; // Высота панели при её создании //--- Возвращает флаги шрифта uint GetFontFlags(void); //--- Создаёт новый графический объект virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Возвращает начальные координаты привязанного объекта virtual void GetCoords(int &x,int &y); //--- Создаёт объект-подложку bool CreateUnderlayObj(void); //--- Привязывает элемент к контейнеру bool SetDockingToContainer(void); protected:
В защищённой секции класса напишем методы для работы с координатами объекта-подложки:
protected: //--- Устанавливает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) все параметры подложки bool SetCoordXUnderlay(const int value) { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordX(value) : false); } bool SetCoordYUnderlay(const int value) { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordY(value) : false); } bool SetWidthUnderlay(const int value) { return(this.m_underlay!=NULL ? this.m_underlay.SetWidth(value) : false); } bool SetHeightUnderlay(const int value) { return(this.m_underlay!=NULL ? this.m_underlay.SetHeight(value) : false); } bool SetUnderlayParams(void); //--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту подложки int GetCoordXUnderlay(void) const { return(this.m_underlay!=NULL ? this.m_underlay.CoordX() : 0); } int GetCoordYUnderlay(void) const { return(this.m_underlay!=NULL ? this.m_underlay.CoordY() : 0); } int GetWidthUnderlay(void) const { return(this.m_underlay!=NULL ? this.m_underlay.Width() : 0); } int GetHeightUnderlay(void) const { return(this.m_underlay!=NULL ? this.m_underlay.Height() : 0); } //--- Возвращает координату (1) X, (2) Y подложки относительно панели int GetCoordXUnderlayRelative(void) const { return(this.m_underlay!=NULL ? this.m_underlay.CoordXRelative() : 0); } int GetCoordYUnderlayRelative(void) const { return(this.m_underlay!=NULL ? this.m_underlay.CoordYRelative() : 0); } public:
В публичной секции класса напишем метод, возвращающий указатель на объект-подложку и объявим виртуальный метод, перемещающий объект-панель:
public: //--- Возвращает подложку CGCnvElement *GetUnderlay(void) { return this.m_underlay; } //--- Обновляет координаты (сдвигает канвас) virtual bool Move(const int x,const int y,const bool redraw=false); //--- (1) Устанавливает, (2) возвращает цвет текста по умолчанию всех объектов на панели void ForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; }
Доработаем некоторые публичные методы.
Метод, устанавливающий режим привязки границ элемента к контейнеру:
//--- (1) Устанавливает, (2) возвращает режим привязки границ элемента к контейнеру void DockMode(const ENUM_CANV_ELEMENT_DOCK_MODE mode) { if(m_dock_mode==mode) return; this.m_dock_mode=mode; this.SetDockingToContainer(); }
Ранее метод просто устанавливал переданное в него значение в соответствующую переменную класса.
Теперь же мы не только будем устанавливать значение в переменную, но и сразу же прикреплять панель к нужным граням его контейнера при помощи метода SetDockingToContainer(), который рассмотрим чуть ниже.
Таким же образом доработаем методы, устанавливающие промежуток слева, сверху, справа и снизу внутри элемента управления:
//--- Устанавливает промежуток (1) слева, (2) сверху, (3) справа, (4) снизу, (5) со всех сторон внутри элемента управления void PaddingLeft(const uint value) { this.m_padding[0]=((int)value<this.m_frame_width_left ? this.m_frame_width_left : (int)value); if(this.m_underlay!=NULL) { //--- Устанавливаем значение смещения подложки по оси X this.m_underlay.SetCoordXRelative(this.PaddingLeft()); //--- Устанавливаем координату X и ширину подложки this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft()); this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight()); } } void PaddingTop(const uint value) { this.m_padding[1]=((int)value<this.m_frame_width_top ? this.m_frame_width_top : (int)value); if(this.m_underlay!=NULL) { //--- Устанавливаем значение смещения подложки по оси Y this.m_underlay.SetCoordYRelative(this.PaddingTop()); //--- Устанавливаем координату Y и высоту подложки this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop()); this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom()); } } void PaddingRight(const uint value) { this.m_padding[2]=((int)value<this.m_frame_width_right ? this.m_frame_width_right : (int)value); if(this.m_underlay!=NULL) { //--- Устанавливаем значение ширины подложки this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight()); } } void PaddingBottom(const uint value) { this.m_padding[3]=((int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (int)value); if(this.m_underlay!=NULL) { //--- Устанавливаем значение высоты подложки this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom()); } } void PaddingAll(const uint value) { this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value); }
В этих методах точно так же, помимо записи переданных в метод значений в соответствующие переменные, мы сразу же изменяем эти свойства у объекта-подложки.
Аналогично доработаем методы, устанавливающие ширину рамки формы слева, сверху, справа, и снизу элемента управления:
//--- Устанавливает ширину рамки формы (1) слева, (2) сверху, (3) справа, (4) снизу, (5) всех сторон элемента управления void FrameWidthLeft(const uint value) { this.m_frame_width_left=(int)value; if(PaddingLeft()<FrameWidthLeft()) PaddingLeft(FrameWidthLeft()); } void FrameWidthTop(const uint value) { this.m_frame_width_top=(int)value; if(this.PaddingTop()<this.FrameWidthTop()) this.PaddingTop(this.FrameWidthTop()); } void FrameWidthRight(const uint value) { this.m_frame_width_right=(int)value; if(this.PaddingRight()<this.FrameWidthRight()) this.PaddingRight(this.FrameWidthRight()); } void FrameWidthBottom(const uint value) { this.m_frame_width_bottom=(int)value; if(this.PaddingBottom()<this.FrameWidthBottom()) this.PaddingBottom(this.FrameWidthBottom()); } void FrameWidthAll(const uint value) { this.FrameWidthLeft(value); this.FrameWidthTop(value); this.FrameWidthRight(value); this.FrameWidthBottom(value); }
Теперь, как только будет изменена ширина рамки с любой стороны панели, тут же будут изменены и соответствующие свойства объекта-подложки таким образом, чтобы подложка всегда была вписана либо в рамку панели (если Padding стороны панели меньше ширины рамки с этой же стороны), либо, в соответствие значению Padding этой стороны панели.
Удалим из листинга два лишних конструктора — их объявление и реализацию за пределами тела класса:
//--- Конструкторы CPanel(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); CPanel(const int subwindow, const string name, const int x, const int y, const int w, const int h); CPanel(const string name, const int x, const int y, const int w, const int h); CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)
Эти конструкторы нам не понадобились.
А вот в параметрическом конструкторе добавим инициализацию свойств объекта-панели, которую ранее не делали, и новых переменных:
CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_DEF_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.DockMode(CANV_ELEMENT_DOCK_MODE_NONE); this.BorderStyle(FRAME_STYLE_BEVEL); this.AutoScroll(false); this.AutoScrollMarginAll(0); this.AutoSize(false); this.AutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW); this.Initialize(); this.CreateUnderlayObj(); this.m_init_x=0; this.m_init_y=0; this.m_init_w=0; this.m_init_h=0; } //--- Деструктор ~CPanel(); }; //+------------------------------------------------------------------+
Точно так же инициализируем эти же переменные и в конструкторе с указанием идентификатора чарта и подокна.
В методе, возвращающем начальные координаты привязанного объекта, теперь будем возвращать координаты относительно координат объекта-подложки, а не самой панели и ширины её рамки как это было ранее:
//+------------------------------------------------------------------+ //| Возвращает начальные координаты привязанного объекта | //+------------------------------------------------------------------+ void CPanel::GetCoords(int &x,int &y) { x=this.m_underlay.CoordX()+x; y=this.m_underlay.CoordY()+y; } //+------------------------------------------------------------------+
Метод, создающий объект-подложку:
//+------------------------------------------------------------------+ //| Создаёт объект-подложку | //+------------------------------------------------------------------+ bool CPanel::CreateUnderlayObj(void) { this.m_underlay=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,this.ID(),this.Number(),this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Undl"), this.CoordX()+this.PaddingLeft(),this.CoordY()+this.PaddingTop(), this.Width()-this.PaddingLeft()-this.PaddingRight(), this.Height()-this.PaddingTop()-this.PaddingBottom(), CLR_CANV_NULL,0,false,false); if(m_underlay==NULL) { CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ); return false; } if(!this.m_list_tmp.Add(this.m_underlay)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); delete this.m_underlay; return false; } this.SetUnderlayParams(); return true; } //+------------------------------------------------------------------+
Здесь: создаём новый объект-графический элемент с координатами и размерами, рассчитанными относительно значений Padding всех сторон панели так, чтобы объект-подложка точно вписывалась в область, ограниченную значениями Padding всех сторон панели.
Если объект создать не удалось — сообщаем об этом и возвращаем false.
Если созданный объект не удалось добавить в список объектов панели — сообщаем об этом, удаляем созданный объект и возвращаем false.
Если всё прошло успешно — вызываем метод для установки всех параметров созданной подложки и возвращаем true.
Метод, устанавливающий все параметры подложки:
//+------------------------------------------------------------------+ //| Устанавливает все параметры подложки | //+------------------------------------------------------------------+ bool CPanel::SetUnderlayParams(void) { //--- Устанавливаем в переменные значения смещения подложки по осям X и Y this.m_underlay.SetCoordXRelative(this.PaddingLeft()); this.m_underlay.SetCoordYRelative(this.PaddingTop()); //--- Устанавливаем координаты и размеры подложки bool res=true; res &=this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft()); res &=this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop()); res &=this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight()); res &=this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom()); return res; } //+------------------------------------------------------------------+
Здесь всё просто: записываем смещения координат подложки относительно координат панели, и координаты и размеры объекта-подложки.
Метод, обновляющий координаты элемента:
//+------------------------------------------------------------------+ //| Обновляет координаты элемента | //+------------------------------------------------------------------+ bool CPanel::Move(const int x,const int y,const bool redraw=false) { if(!this.m_underlay.Move(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative())) return false; //--- Получаем указатели на базовый и главный объекты в иерархии связанных объектов и объект-тень CGCnvElement *base=this.GetBase(); CGCnvElement *main=this.GetMain(); CShadowObj *shadow=this.GetShadowObj(); //--- Если элемент не перемещаемый, и это базовый объект - уходим if(!this.Movable() && main==NULL) return false; //--- Если есть тень у объекта и не удалось записать новые значения координат в свойства объекта-тени - возвращаем false if(this.m_shadow) { if(shadow==NULL || !shadow.Move(x-OUTER_AREA_SIZE+shadow.CoordXRelative(),y-OUTER_AREA_SIZE+shadow.CoordYRelative(),false)) return false; } //--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- Смещаем все привязанные объекты if(!this.MoveDependentObj(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative(),false)) return false; //--- Если стоит флаг обновления, и это главный объект иерархии - перерисовываем график. if(redraw && main==NULL) ::ChartRedraw(this.ChartID()); //--- Возвращаем true return true; } //+------------------------------------------------------------------+
Логика метода подробно расписана в комментариях к коду. Здесь проверяем перемещаемость объекта, и если он не перемещаемый, то уходим из метода. Затем смещаем тень и сам объект. После чего вызываем метод смещения всех привязанных объектов его иерархии. По окончанию проверяем, что это главный объект иерархии всех связанных объектов, и обновляем график.
Метод, привязывающий элемент к контейнеру:
//+------------------------------------------------------------------+ //| Привязывает элемент к контейнеру | //+------------------------------------------------------------------+ bool CPanel::SetDockingToContainer(void) { //--- Получаем указатель на объект-панель, к которуму привязан этот объект CPanel *base=this.GetBase(); if(base==NULL) return false; //--- Объявляем переменные и получаем в них координаты и размеры базового объекта int x=base.GetCoordXUnderlay(); int y=base.GetCoordYUnderlay(); int w=base.GetWidthUnderlay(); int h=base.GetHeightUnderlay(); //--- В зависимости от установленного режима привязки к контейнеру перемещаем объект к нужным граням базового объекта switch(this.DockMode()) { //--- Присоединение сверху и растягивание на ширину контейнера case CANV_ELEMENT_DOCK_MODE_TOP : this.SetWidth(w); this.SetHeight(this.m_init_h); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay(); y=base.GetCoordYUnderlay(); this.Move(x,y); this.SetCoordXRelative(0); this.SetCoordYRelative(0); break; //--- Присоединение снизу и растягивание на ширину контейнера case CANV_ELEMENT_DOCK_MODE_BOTTOM : this.SetWidth(w); this.SetHeight(this.m_init_h); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay(); y=base.GetCoordYUnderlay()+(base.GetHeightUnderlay()-this.Height()); this.Move(x,y); this.SetCoordXRelative(0); this.SetCoordYRelative(base.GetHeightUnderlay()-this.Height()); break; //--- Присоединение слева и растягивание на высоту контейнера case CANV_ELEMENT_DOCK_MODE_LEFT : this.SetHeight(h); this.SetWidth(this.m_init_w); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay(); y=base.GetCoordYUnderlay(); this.Move(x,y); this.SetCoordXRelative(0); this.SetCoordYRelative(0); break; //--- Присоединение справа и растягивание на высоту контейнера case CANV_ELEMENT_DOCK_MODE_RIGHT : this.SetHeight(h); this.SetWidth(this.m_init_w); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay()+(base.GetWidthUnderlay()-this.Width()); y=base.GetCoordYUnderlay(); this.Move(x,y); this.SetCoordXRelative(base.GetWidthUnderlay()-this.Width()); this.SetCoordYRelative(0); break; //--- Растягивание на ширину и высоту всего контейнера case CANV_ELEMENT_DOCK_MODE_FILL : this.SetWidth(w); this.SetHeight(h); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay(); y=base.GetCoordYUnderlay(); this.Move(x,y); this.SetCoordXRelative(0); this.SetCoordYRelative(0); break; //--- Прикреплён к указанным координатам, размеры не меняются default: // CANV_ELEMENT_DOCK_MODE_NONE this.SetHeight(this.m_init_h); this.SetWidth(this.m_init_w); this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle()); this.Update(); this.Done(); x=base.GetCoordXUnderlay()+this.CoordXRelativeInit(); y=base.GetCoordYUnderlay()+this.CoordYRelativeInit(); this.Move(x,y); this.SetCoordXRelative(this.CoordXRelativeInit()); this.SetCoordYRelative(this.CoordYRelativeInit()); break; } ::ChartRedraw(this.ChartID()); return true; } //+------------------------------------------------------------------+
Логика метода тоже расписана в комментариях к коду. Суть в том, что в зависимости от режима привязки объекта к контейнеру, рассчитываем нужные соответствующие координаты и размеры и перемещаем объект в новые координаты. Если объект не привязан к сторонам контейнера, то получаем его изначальные координаты и размеры, которые были установлены при его создании.
Метод всегда вызывается при установке нового значения для свойства DockMode объекта.
Теперь подкорректируем методы в классе-коллекции графических элементов в \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
Так как у нас теперь есть возможность одновременной установки как единственного цвета фона формы, так и цветов его градиентной заливки, то во всех методах создания объектов-форм с градиентной заливкой заменим указание цвета методом
obj.SetColorBackground(clr[0]);
на метод SetColorsBackground():
//--- Создаёт объект-графический объект-форму на канвасе на указанном графике и подокне с заливкой вертикальным градиентом int CreateFormVGradient(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h, color &clr[], const uchar opacity, const bool movable, const bool activity, const bool shadow=false, const bool redraw=false) { int id=this.m_list_all_canv_elm_obj.Total(); CForm *obj=new CForm(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorsBackground(clr); obj.SetColorFrame(clr[0]); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity()); obj.Done(); obj.Erase(clr,opacity,true,false,redraw); return obj.ID(); }
Теперь в метод передаётся не первый цвет из массива цветов, а сам массив.
Такие изменения уже сделаны во всех методах создания объектов-форм.
Похожие доработки сделаем и в методах создания объектов-панелей:
//--- Создаёт объект-графический объект WinForms Panel на канвасе на указанном графике и подокне с заливкой вертикальным градиентом int CreatePanelVGradient(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h, color &clr[], const uchar opacity, const bool movable, const bool activity, const int frame_width=-1, ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, const bool shadow=false, const bool redraw=false) { int id=this.m_list_all_canv_elm_obj.Total(); CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorsBackground(clr); obj.SetColorFrame(clr[0]); obj.BorderStyle(frame_style); obj.SetOpacity(opacity,false); //--- Установим флаг рисования тени obj.SetShadow(shadow); if(shadow) { //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный //--- и затемним монохромный цвет на 20 единиц color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-100),-20); //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям //--- Непрозрачность тени при этом установим равной значению по умолчанию, а радиус размытия равный 4 obj.DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR); } obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,true,false,redraw); if(frame_width>0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),obj.BorderStyle()); obj.Done(); return obj.ID(); }
Здесь мы тоже передаём в свойства объекта массив цветов градиентной заливки, плюс устанавливаем в свойства объекта тип рамки как рельефная и используем этот тип при рисовании рамки. Ранее мы просто рисовали рамку с переданным в метод типом, но в сам объект тип рамки не записывали, что при повторной перерисовке объекта не рисовало рамку, так как свойство было по умолчанию — отсутствие рамки, и оно никак не изменялось здесь. Сейчас мы это исправили.
Такие изменения уже внесены во все методы создания панелей с градиентной заливкой, и с ними можно ознакомиться в прилагаемых к статье файлах.
Чтобы мы могли проконтролировать правильность назначения объектам, привязанным к панели, их идентификаторов и свойств ZOrder, временно пропишем код для вывода текстов на привязанные к панели объекты.
В методе, устанавливающим ZOrder в указанный элемент, а в остальных элементах корректирующем, впишем такие блоки кода:
//+------------------------------------------------------------------+ //| Устанавливает ZOrder в указанный элемент, | //| а в остальных элементах корректирует | //+------------------------------------------------------------------+ bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj) { //--- Получаем максимальный ZOrder всех графических элементов long max=this.GetZOrderMax(); //--- Если передан невалидный указатель на объект, или не получен максимальный ZOrder - возвращаем false if(obj==NULL || max<0) return false; //--- Объявляем переменную для хранения результата работы метода bool res=true; //--- Если максимальный ZOrder нулевой, то ZOrder будет равен 1, //--- если максимальный ZOrder меньше значения (общее количества графических элементов)-1, то ZOrder будет на 1 больше, //--- иначе - ZOrder будет равен значению (общее количества графических элементов)-1 long value=(max==0 ? 1 : max<this.m_list_all_canv_elm_obj.Total()-1 ? max+1 : this.m_list_all_canv_elm_obj.Total()-1); //--- Если не удалось выставить ZOrder объекту, переданному в метод - возвращаем false if(!obj.SetZorder(value,false)) return false; //--- Временно объявим объект-форму - для рисования на нём текста для визуального отображения его ZOrder CForm *form=obj; //--- и нарисуем на форме текст с указанием ZOrder form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true); //--- Временно - для теста, если этот элемент - форма и выше if(form.Type()>=OBJECT_DE_TYPE_GFORM) { for(int j=0;j<form.ElementsTotal();j++) { CForm *pnl=form.GetElement(j); if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL) continue; pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity()); } } //--- Отсортируем список графических элементов по идентификатору элемента this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID); //--- Получим список графических элементов без объекта, идентификатор которого равен идентификатору объекта, переданного в метод CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID,obj.ID(),NO_EQUAL); //--- Если отфильтрованный список получить не удалось и при этом размер списка больше одного, //--- что означает наличие в нём других объектов помимо отфильтрованного по ID - возвращаем false if(list==NULL && this.m_list_all_canv_elm_obj.Total()>1) return false; //--- В цикле по полученному списку оставшихся объектов-графических элементов for(int i=0;i<list.Total();i++) { //--- получаем очередной объект CGCnvElement *elm=list.At(i); //--- Если объект получить не удалось, или это контрольная форма управления опорными точками расширенного стандартного графического объекта //--- или, если ZOrder объекта нулевой - пропускаем этот объект, так как изменять его ZOrder не нужно - он самий нижний if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0) continue; //--- Если не удалось установить ZOrder объекту на 1 меньше, чем он есть (ZOrder уменьшаем на 1) - добавляем к значению res значение false if(!elm.SetZorder(elm.Zorder()-1,false)) res &=false; //--- Временно - для теста, если этот элемент - форма и выше if(elm.Type()>=OBJECT_DE_TYPE_GFORM) { //--- присвоим форме указатель на элемент и нарисуем на форме текст с указанием ZOrder form=elm; form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true); for(int j=0;j<form.ElementsTotal();j++) { CForm *pnl=form.GetElement(j); if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL) continue; pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity()); } } } //--- По окончании цикла возвращаем результат, записанный в res return res; } //+------------------------------------------------------------------+
Этот код позволяет найти привязанный объект к панели и вывести на него надпись с идентификатором найденного объекта и значением его ZOrder.
Не всегда вовремя срабатывает вывод надписей, но это сейчас и не важно — это нужно лишь один раз и временно — далее эти коды удалим из метода.
Теперь у нас всё готово для тестирования.
Тестирование
Возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part104\ под новым именем TestDoEasyPart104.mq5.
Как будем тестировать. В прошлом советнике мы уже создавали одну панель, а в ней ещё несколько. Теперь же мы создадим внутри панели один объект-панель и назначим клавиши для его привязки к граням главной панели. Нажимая клавиши на клавиатуре мы будем устанавливать все возможные типы привязки зависимой панели к сторонам главной. Главной панели назначим значение Padding, равное 10 — чтобы был виден отступ от краёв панели и было понятно, как работает Padding при позиционировании одного объекта внутри другого.
Клавиши назначим такие:
- W — для привязки к верхней грани,
- A — для привязки к левой грани,
- D — для привязки к правой грани,
- X — для привязки к нижней грани,
- S — для заполнения,
- Z — для сброса привязки и возврата к исходным размерам и координатам.
В глобальной области назначим коды клавиш:
//+------------------------------------------------------------------+ //| TestDoEasyPart104.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/ru/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/ru/users/artmedia70" #property version "1.00" //--- includes #include <DoEasy\Engine.mqh> //--- defines #define FORMS_TOTAL (3) // Количество создаваемых форм #define START_X (4) // Начальная координата X фигуры #define START_Y (4) // Начальная координата Y фигуры #define KEY_LEFT (65) // (A) Влево #define KEY_RIGHT (68) // (D) Вправо #define KEY_UP (87) // (W) Вверх #define KEY_DOWN (88) // (X) Вниз #define KEY_CENTER (83) // (S) Центр #define KEY_ORIGIN (90) // (Z) По умолчанию //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CEngine engine; color array_clr[]; //+------------------------------------------------------------------+
В OnInit() советника создадим все объекты (было ранее) и создадим внутри панели ещё одну панель:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Установка глобальных переменных советника ArrayResize(array_clr,2); // Массив цветов градиентной заливки array_clr[0]=C'26,100,128'; // Исходный ≈Тёмно-лазурный цвет array_clr[1]=C'35,133,169'; // Осветлённый исходный цвет //--- Создадим массив с текущим символом и установим его для использования в библиотеке string array[1]={Symbol()}; engine.SetUsedSymbols(array); //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания //--- Создадим объекты-формы string name=""; int obj_id=WRONG_VALUE; CArrayObj *list=NULL; CForm *form=NULL; for(int i=0;i<FORMS_TOTAL;i++) { form=engine.CreateWFForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30,array_clr,245,true); if(form==NULL) continue; //--- Установим ZOrder в ноль и выведем текст с описанием типа градиента и обновим форму //--- Параметры текста: координаты текста в центре формы и точка привязки - тоже по центру //--- Создаём новый кадр текстовой анимации с идентификатором 0 и выводим текст на форму form.SetZorder(0,false); form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,false); } //--- Создадим четыре графических элемента CGCnvElement *elm=NULL; array_clr[0]=C'0x65,0xA4,0xA9'; array_clr[1]=C'0x48,0x75,0xA2'; //--- Вертикальный градиент elm=engine.CreateWFElement("CElmVG",form.RightEdge()+20,20,200,50,array_clr,127,true); if(elm!=NULL) { elm.SetFontSize(10); elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } //--- Вертикальный циклический градиент elm=engine.CreateWFElement("CElmVGC",form.RightEdge()+20,80,200,50,array_clr,127,true,true); if(elm!=NULL) { elm.SetFontSize(10); elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } //--- Горизонтальный градиент elm=engine.CreateWFElement("CElmHG",form.RightEdge()+20,140,200,50,array_clr,127,false,false); if(elm!=NULL) { elm.SetFontSize(10); elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } //--- Горизонтальный циклический градиент elm=engine.CreateWFElement("CElmHGC",form.RightEdge()+20,200,200,50,array_clr,127,false,true); if(elm!=NULL) { elm.SetFontSize(10); elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } //--- Создадим объект WinForms Panel CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",elm.RightEdge()+20,50,230,150,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true); if(pnl!=NULL) { //--- Установим значение Padding равным 10 pnl.PaddingAll(10); pnl.FontDrawStyle(FONT_STYLE_NORMAL); pnl.Bold(true); pnl.SetFontSize(10); pnl.TextOnBG(0,pnl.TypeElementDescription()+": ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',pnl.Opacity()); //--- В цикле создадим N привязанных объектов-панелей (одну панель) CPanel *obj=NULL; for(int i=0;i<1;i++) { //--- создадим объект-панель с координатами по оси X по центру, и 10 по оси Y, шириной 80 и высотой 50 pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_PANEL,pnl,pnl.GetUnderlay().Width()/2-40,10,80,50,C'0xCD,0xDA,0xD7',200,true); //--- Для контроля за созданием привязанных объектов //--- получим указатель на привязанный объект по индексу цикла obj=pnl.GetElement(i); //--- из полученного объекта возьмём указатель на его базовый объект //--- и выведем в журнал наименование созданного привязанного объекта и имя его базового объекта Print ( TextByLanguage("Объект ","Object "),obj.TypeElementDescription()," ",obj.Name(), TextByLanguage(" привязан к объекту "," is attached to object "),obj.GetBase().TypeElementDescription()," ",obj.GetBase().Name() ); if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_PANEL) { //--- Выведем на созданную панель идентификатор и zorder obj.TextOnBG(0,"ID "+(string)obj.ID()+", ZD "+(string)obj.Zorder(),obj.Width()/2,obj.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',obj.Opacity()); //--- Установим цвет рамки, активную зону панели и нарисуем рамку obj.SetColorFrame(obj.ColorBackground()); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),FRAME_STYLE_BEVEL); obj.Update(); } } pnl.Update(true); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
В обработчике OnChartEvent() впишем такой код обработки нажатия клавиш:
//--- Если нажата клавиша на клавиатуре if(id==CHARTEVENT_KEYDOWN) { CPanel *panel=engine.GetWFPanel(7).GetElement(0); if(panel!=NULL) { if(lparam==KEY_UP) panel.DockMode(CANV_ELEMENT_DOCK_MODE_TOP); else if(lparam==KEY_DOWN) panel.DockMode(CANV_ELEMENT_DOCK_MODE_BOTTOM); else if(lparam==KEY_LEFT) panel.DockMode(CANV_ELEMENT_DOCK_MODE_LEFT); else if(lparam==KEY_RIGHT) panel.DockMode(CANV_ELEMENT_DOCK_MODE_RIGHT); else if(lparam==KEY_CENTER) panel.DockMode(CANV_ELEMENT_DOCK_MODE_FILL); else if(lparam==KEY_ORIGIN) panel.DockMode(CANV_ELEMENT_DOCK_MODE_NONE); }
Здесь мы получаем объект-панель по его идентификатору (мы точно знаем, что его идентификатор равен 7), получаем из его списка привязанных объектов самый первый (и единственный) и, в зависимости от кода нажатой кнопки, устанавливаем для полученного объекта-панели режим DockMode.
Скомпилируем советник, запустим его на графике и понажимаем клавиши на клавиатуре:
Как видим, при нажатии разных клавиш и установке соответствующих способов привязки, панель верно позиционируется внутри своего контейнера, а при нажатии клавиши Z возвращается к исходным размерам и координатам. При этом панель не прилипает непосредственно к граням контейнера, а располагается на дистанции Padding от краёв главной панели. При перемещении главной панели, привязанная к ней панель тоже перемещается корректно на вновь заданных координатах в зависимости от текущего режима привязки.
Что дальше
В следующей статье продолжим работу над WinForms-объектами.
*Статьи этой серии:
DoEasy. Элементы управления (Часть 1): Первые шаги
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования