
DoEasy. Controls (Part 4): Panel control, Padding and Dock parameters
Contents
Concept
I continue my work on the functionality of the WinForms Panel control. In the current article, I will consider its Padding and Dock properties.
The Panel WinForm object is essentially a regular container for placing other WinForm objects inside it. When placing such objects, we can independently specify the required coordinates for placing an object, so that it is located at the specified coordinates. But we can also specify how to bind the object placed in the container after it has been created inside the container. There are six methods of binding an object inside its container (object's Dock property):
- Attaching to the upper border and stretching to the container width,
- Attaching to the lower border and stretching to the container width,
- Attaching to the left border and stretching to the container height,
- Attaching to the right border and stretching to the container height,
- Stretching along the entire container width and height (filling),
- The object is attached to the specified coordinates and its size does not change.
If we select one of the bind methods, in which an object sticks to one or all container borders, its edges are bound to the container border considering the Padding value set for the container — the border of the placed object is not bound to the container border, but is distanced from the container by a distance specified in container Padding value.
The way an object is located inside the container is specified in its Dock value. If the container features more than one object, each subsequent object "sticks" not to the container border at the Padding distance, but to the previous object attracted to the same side of the container.
The image shows how objects in MS Visual Studio are attracted to the top edge of their container having the value of Padding set to 20 if attraction to the top edge of the container is set for them:
In the current article, I will implement all possible options of locating an object inside its one-object container.
In order to consider the Padding value of the container, I will add yet another object on canvas to the Panel object itself to be served as an underlay for placing all the necessary objects inside the panel, rather than shifting the coordinates of the object located inside the panel.
I think, this will be more convenient for arranging objects while considering the Padding value of the container. The underlay object itself is shifted by the property value. Besides, when we need to draw anything on the canvas, we will do that on the underlay — it will already be shifted by Padding value and there will be no need to calculate coordinates and size of the object image. Besides, there are some other conveniences we will evaluate later when developing other WinForm objects.
Improving library classes
We have the ability to create various styles of form objects. A separate library file allows us to gradually set additional style parameters. Since the form object can also be filled with a gradient filling, set the new style parameters for the gradient filling of the form object background in \MQL5\Include\DoEasy\GraphINI.mqh:
//+------------------------------------------------------------------+ //| List of form style parameter indices | //+------------------------------------------------------------------+ enum ENUM_FORM_STYLE_PARAMS { //--- CForm FORM_STYLE_FRAME_SHADOW_OPACITY, // Shadow opacity FORM_STYLE_FRAME_SHADOW_BLUR, // Shadow blur FORM_STYLE_DARKENING_COLOR_FOR_SHADOW, // Form shadow color darkening FORM_STYLE_FRAME_SHADOW_X_SHIFT, // Shadow X axis shift FORM_STYLE_FRAME_SHADOW_Y_SHIFT, // Shadow Y axis shift FORM_STYLE_GRADIENT_V, // Vertical gradient filling flag FORM_STYLE_GRADIENT_C, // Cyclic gradient filling flag //--- CPanel FORM_STYLE_FRAME_WIDTH_LEFT, // Panel frame width to the left FORM_STYLE_FRAME_WIDTH_RIGHT, // Panel frame width to the right FORM_STYLE_FRAME_WIDTH_TOP, // Panel frame width on top FORM_STYLE_FRAME_WIDTH_BOTTOM, // Panel frame width below }; #define TOTAL_FORM_STYLE_PARAMS (11) // Number of form style parameters //+------------------------------------------------------------------+ //| Array containing form style parameters | //+------------------------------------------------------------------+ int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]= { //--- "Flat form" style parameters { //--- CForm 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift false, // Vertical gradient filling flag false, // Cyclic gradient filling flag //--- CPanel 3, // Panel frame width to the left 3, // Panel frame width to the right 3, // Panel frame width on top 3, // Panel frame width below }, //--- "Embossed form" style parameters { //--- CForm 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift true, // Vertical gradient filling flag false, // Cyclic gradient filling flag //--- CPanel 3, // Panel frame width to the left 3, // Panel frame width to the right 3, // Panel frame width on top 3, // Panel frame width below }, }; //+------------------------------------------------------------------+
Increase the number of style parameters from 9 to 11.
In \MQL5\Include\DoEasy\Defines.mqh, namely in the enumeration of control borders bound to the container, move the constant specifying the absence of binding the object to the container borders in the first place:
//+------------------------------------------------------------------+ //| Control borders bound to the container | //+------------------------------------------------------------------+ enum ENUM_CANV_ELEMENT_DOCK_MODE { CANV_ELEMENT_DOCK_MODE_NONE, // Attached to the specified coordinates, size does not change CANV_ELEMENT_DOCK_MODE_TOP, // Attaching to the top and stretching along the container width CANV_ELEMENT_DOCK_MODE_BOTTOM, // Attaching to the bottom and stretching along the container width CANV_ELEMENT_DOCK_MODE_LEFT, // Attaching to the left and stretching along the container height CANV_ELEMENT_DOCK_MODE_RIGHT, // Attaching to the right and stretching along the container height CANV_ELEMENT_DOCK_MODE_FILL, // Stretching along the entire container width and height }; //+------------------------------------------------------------------+
Previously, the constant occupied the last place in the list. This is not very convenient if we want to make sure that the object is not bound to the container borders. Now we can simply check the method returning the type of binding an object to the container. The value of 0 the constant corresponds to is equal to false in boolean equivalent.
In E:\MetaQuotes\MetaTrader 5\MQL5\Include\DoEasy\Data.mqh, add the new message indices:
MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE, // Failed to change the color array size MSG_LIB_SYS_FAILED_ARRAY_RESIZE, // Failed to change the array size MSG_LIB_SYS_FAILED_ARRAY_COPY, // Failed to copy the array MSG_LIB_SYS_FAILED_ADD_BUFFER, // Failed to add buffer object to the list MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ, // Failed to create \"Indicator buffer\" object
...
//--- CGCnvElement MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY, // Error! Empty array MSG_CANV_ELEMENT_ERR_ARRAYS_NOT_MATCH, // Error! Array-copy of the resource does not match the original MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH, // Error! Failed to set the canvas width MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT, // Error! Failed to set the canvas height
...
//--- CGStdGraphObjExtToolkit MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA, // Failed to change the size of the pivot point time data array MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA, // Failed to change the size of the pivot point price data array MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM, // Failed to create a form object to manage a pivot point //--- CPanel MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ, // Failed to create the underlay object }; //+------------------------------------------------------------------+
and text messages corresponding to the newly added indices:
{"Не удалось изменить размер массива цветов","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"}, }; //+---------------------------------------------------------------------+
When changing the size of the form object and of its descendants, we need to completely redraw the object. In order to restore its original size later on, we will need to know its original coordinates and size. In addition, since the form can be filled with either a single color or with a gradient filling, it is better to have an array containing all colors of the gradient filling. If there is one color, it is not set in the array. Thus, we will always have only an initial color in the object properties, while the array will contain all the colors used for the gradient. Thus, we need to improve the method for setting a single color and add yet another method for setting gradient colors. Both methods will fill in the values for both one color and the gradient.
In addition, different objects can be attached to each other. For example, one panel object features two attached WinForm objects. The panel object itself is attached to yet another panel. For the two objects attached to the first panel, it will be the base object, so it will be specified in their properties as the base object used as a container for them. When we place this container with two objects inside yet another container, the new panel object becomes the base container for the first container. To let the two objects placed in the first container know which objects is a base one in this hierarchy, we need to add yet another property — the main object of the entire hierarchy. In this case, two objects located in the first container will know its base container and the main object — the second container their base one is bound to.
Let's improve the graphical element object class in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
Declare the new variables in its protected section:
//+------------------------------------------------------------------+ //| Class of the graphical element object | //+------------------------------------------------------------------+ class CGCnvElement : public CGBaseObj { protected: CGCnvElement *m_element_main; // Pointer to the initial parent element within all the groups of bound objects CGCnvElement *m_element_base; // Pointer to the parent element within related objects of the current group CCanvas m_canvas; // CCanvas class object CPause m_pause; // Pause class object bool m_shadow; // Shadow presence color m_chart_color_bg; // Chart background color uint m_duplicate_res[]; // Array for storing resource data copy color m_array_colors_bg[]; // Array of element background colors bool m_gradient_v; // Vertical gradient filling flag bool m_gradient_c; // Cyclic gradient filling flag int m_init_relative_x; // Initial relative X coordinate int m_init_relative_y; // Initial relative Y coordinate //--- Create (1) the object structure and (2) the object from the structure virtual bool ObjectToStruct(void); virtual void StructToObject(void); private:
In the private section of the class, declare the method for saving the array of gradient filling colors:
//--- Return the index of the array the order's (1) double and (2) string properties are located at 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; } //--- Save the colors to the background color array void SaveColorsBG(color &colors[]); public:
In the public section of the class, write the methods setting and returning the values of newly added variables:
//--- Create the element 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) Set and (2) return the initial shift of the (1) X and (2) Y coordinate relative to the base object 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) Set and (2) return the pointer to the parent element within related objects of the current group void SetBase(CGCnvElement *element) { this.m_element_base=element; } CGCnvElement *GetBase(void) { return this.m_element_base; } //--- (1) Set and (2) return the pointer to the parent element within all groups of related objects void SetMain(CGCnvElement *element) { this.m_element_main=element; } CGCnvElement *GetMain(void) { return this.m_element_main; } //--- Return the pointer to a canvas object CCanvas *GetCanvasObj(void) { return &this.m_canvas; }
In the default constructor, use the value of NULL to initialize the pointer to the main object of the related objects hierarchy:
//--- Default constructor/Destructor 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(); }
Improve the method setting the graphical element background color:
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); }
Now it not only sets the background color, passed to the method, to the object properties, but also calls the method, which sets the only color to the new array of gradient filling colors. To achieve this, declare the array, set the color value to it and pass the array to the SaveColorsBG() method that I will describe a bit later.
Now let's set the method setting the colors of the graphical element background gradient filling:
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]; }
The method receives the array of gradient filling colors that we pass to the SaveColorsBG() method. The very first color from the color array is then set to the background color value. Thus, we set several different colors at once: if only one color is used for the graphical element background, this will be the first color from the array. In case of the gradient filling, all colors passed to the array method are set to the array of gradient filling colors in the SaveColorsBG() method.
Below, I will set two other methods — the method returning the number of gradient filling colors and the method returning the color from the color array by specified index:
//--- Return the number of colors set for the background gradient filling uint ColorsBackgroundTotal(void) const { return this.m_array_colors_bg.Size(); } //--- Return (1) the background color, (2) the opacity, coordinate (3) of the right and (4) bottom element edge 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; }
The first method simply returns the size of the object color array.
In the second method, if the object color array size is zero, the value from the m_color_bg variable is returned. If the index is invalid (exceeds the array size), the very last color is returned, otherwise the color located in the array by the specified index.
In the parametric constructor, initialize the pointer to the main object of the attached objects hierarchy and set the color from the m_color_bg variable to the color array of the object background gradient filling:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ 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)) {
Set the same strings in the protected constructor (there is no point in showing this again here).
Remove the object immovability check from the method updating the object coordinates:
//+------------------------------------------------------------------+ //| Update the coordinate elements | //+------------------------------------------------------------------+ bool CGCnvElement::Move(const int x,const int y,const bool redraw=false) { //--- Leave if the element is not movable or inactive if(!this.Movable()) return false; //--- If failed to set new values into graphical object properties, return 'false' if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- If the update flag is activated, redraw the chart. if(redraw) ::ChartRedraw(this.ChartID()); //--- Return 'true' return true; } //+------------------------------------------------------------------+
The fact that this check is located inside this particular method is very logical, but this creates considerable issues when moving objects related to one common hierarchy. If we move a parent object containing nested objects, while some of them are non-movable, tracking their immovability flags by presence of a pointer to parent and main objects becomes very difficult.
For now, I decided to track the immovability of an object when actually trying to move it with the mouse rather than perform numerous checks and ignore movement prohibitions for all objects nested in the hierarchy.
Let's improve the methods setting a new width and height:
//+------------------------------------------------------------------+ //| Set a new width | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+ //| Set a new height | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+
Previously, both methods immediately returned the result of changing the canvas height or width:
return this.m_canvas.Resize(width,this.m_canvas.Height());
However, this did not change values set in the object properties.
Therefore, we first check if the value set in the object property equal to the value passed to the method. If the values are equal, there is nothing to change — return true at once.
Next, if failed to change the canvas size, inform of that and return false.
If successful, set the new value to the object properties and return true.
When we call the Erase() method of the CCanvas class, we thereby fill the form with the specified color and opacity. Thus, if the specified color is different from the one set in the m_color_bg variable (or in the color array), the form is filled with that color. When passing colors to the array method, store these colors in the internal array of the two Erase() methods in the graphical element object:
//+------------------------------------------------------------------+ //| Clear the element filling it with color and opacity | //+------------------------------------------------------------------+ 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); } //+------------------------------------------------------------------+
In the method filling the form with a single color, set the same color to the color array of the object gradient filling. Thus, we are able to change the initial object color by painting the form with a color different than the original one.
In another method, save the values, passed to the method, to the variables storing the gradient filling type. Then save the color array to the object array:
//+------------------------------------------------------------------+ //| Clear the element with a gradient fill | //+------------------------------------------------------------------+ void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false) { //--- Set the vertical and cyclic gradient filling flags this.m_gradient_v=vgradient; this.m_gradient_c=cycle; //--- Check the size of the color array int size=::ArraySize(colors); //--- ... //--- ... //--- Save the background color array this.SaveColorsBG(colors); //--- If specified, update the canvas this.Update(redraw); } //+------------------------------------------------------------------+
The flags of gradient filling types saved in the method will allow us to re-create the object with the same gradient values it had before the shape was re-drawn and resized.
The method saving the colors to the background color array:
//+------------------------------------------------------------------+ //| Save the colors to the background color array | //+------------------------------------------------------------------+ 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); } //+------------------------------------------------------------------+
Here, if the size of the color array passed to the method does not match the size of the object color array, change the size of the object gradient filling color array and copy the array, passed to the method, to the object color array.
Since now we have the methods saving and returning the shift of the object coordinates relative to another object in pixels, remove the variables storing the values of the shadow shift relative to the object that casts the shadow and replace them with these methods in the \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh shadow object class file.
Remove the variables from the private section of the class:
//+------------------------------------------------------------------+ //| Shadows object class | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { private: color m_color_shadow; // Shadow color uchar m_opacity_shadow; // Shadow opacity int m_offset_x; // Shadow X axis shift int m_offset_y; // Shadow Y axis shift //--- Gaussian blur bool GaussianBlur(const uint radius);
Remove the methods returning the values of removed variables from the public section of the class:
public: //--- Constructor CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); //--- Supported object properties (1) integer and (2) string ones virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Return the shadow shift by (1) X and (2) Y int OffsetX(void) const { return this.m_offset_x; } int OffsetY(void) const { return this.m_offset_y; } //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const uchar blur_value);
Remove initialization of these variables in the class constructor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ 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(); } //+------------------------------------------------------------------+
In the method drawing the object shadow, replace the strings setting the values to variables
this.m_offset_x=shift_x; this.m_offset_y=shift_y;
and the string, in which the values set for the variables are used
CGCnvElement::Move(this.CoordX()+this.m_offset_x,this.CoordY()+this.m_offset_y);
with setting the values by calling methods and reading values of the shift specified in the parent class instead of reading the values of the now removed variables:
//+------------------------------------------------------------------+ //| Draw the object shadow | //+------------------------------------------------------------------+ void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value) { //--- Set the shadow shift values to the variables by X and Y axes this.SetCoordXRelative(shift_x); this.SetCoordYRelative(shift_y); //--- Calculate the height and width of the drawn rectangle int w=this.Width()-OUTER_AREA_SIZE*2; int h=this.Height()-OUTER_AREA_SIZE*2; //--- Draw a filled rectangle with calculated dimensions this.DrawShadowFigureRect(w,h); //--- Calculate the blur radius, which cannot exceed a quarter of the OUTER_AREA_SIZE constant int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value); //--- If failed to blur the shape, exit the method (GaussianBlur() displays the error on the journal) if(!this.GaussianBlur(radius)) return; //--- Shift the shadow object by X/Y offsets specified in the method arguments and update the canvas CGCnvElement::Move(this.CoordX()+this.CoordXRelative(),this.CoordY()+this.CoordYRelative()); CGCnvElement::Update(); } //+------------------------------------------------------------------+
Now the values of coordinate shifts relative to the base object will always be available for each object whose parent is a graphical element object class. I have removed these variables here since they are no longer needed.
Let's improve the form object class in \MQL5\Include\DoEasy\Objects\Graph\Form.mqh.
Since WinForms objects are inherited from the class, we need to relocate the list for storing the pointers to objects created inside the form from the private section to the protected one. Also, declare the method updating the coordinates of bound objects:
protected: CArrayObj m_list_tmp; // List for storing the pointers int m_frame_width_left; // Form frame width to the left int m_frame_width_right; // Form frame width to the right int m_frame_width_top; // Form frame width at the top int m_frame_width_bottom; // Form frame width at the bottom //--- Initialize the variables void Initialize(void); void Deinitialize(void); //--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); //--- Return the name of the dependent object string CreateNameDependentObject(const string base_name) const { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name; } //--- Update coordinates of bound objects virtual bool MoveDependentObj(const int x,const int y,const bool redraw=false); public:
When creating a new object bound to the form, we need to specify an object it is bound to (its main object).
To do this, pass the pointer to the object it is created from in the method for creating a new object:
//--- Create a new attached element 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);
In the variable initialization method, initialize the flags of the form background gradient filling type using the default values:
//+------------------------------------------------------------------+ //| Initialize the variables | //+------------------------------------------------------------------+ 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); } //+------------------------------------------------------------------+
The vertical non-cyclic gradient is to be used for the gradient filling by default.
I have previously forgotten to specify the movability flag in the method creating a new graphical object. Let's fix this:
//+------------------------------------------------------------------+ //| Create a new graphical object | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+
In the method creating a new bound element, add the pointer to its main parent object of the bound objects hierarchy and set the properties I have missed previously:
//+------------------------------------------------------------------+ //| Create a new attached 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) { //--- If the type of a created graphical element is less than the "element", inform of that and return '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; } //--- Specify the element index in the list int num=this.m_list_elements.Total()+1; //--- Create a graphical element name string ns=(::StringLen((string)num)<2 ? ::IntegerToString(num,2,'0') : (string)num); string name="Elm"+ns; //--- Get the screen coordinates of the object relative to the coordinate system of the base object int elm_x=x; int elm_y=y; this.GetCoords(elm_x,elm_y); //--- Create a new graphical element CGCnvElement *obj=this.CreateNewGObject(element_type,num,name,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) return false; //--- and add it to the list of bound graphical elements if(!this.AddNewElement(obj,elm_x,elm_y)) { delete obj; return false; } //--- Set the minimum properties for a bound graphical element 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); //--- Draw an added object and return 'true' obj.Erase(colour,opacity,true); return true; } //+------------------------------------------------------------------+
Since I have added two new values to the form styles, let's add setting these values to the method specifying the form style and call the Erase() method filling the form with the gradient filling. If the gradient filling color array contains a single color, the form is painted with this one color without a gradient:
//+------------------------------------------------------------------+ //| Set the form style | //+------------------------------------------------------------------+ 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) { //--- Set opacity parameters and the size of the form frame side 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]; //--- Create the shadow object this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]); //--- Set a color scheme this.SetColorTheme(theme,opacity); //--- Calculate a shadow color with color darkening 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); //--- Draw a rectangular 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]); //--- Fill in the form background with color and opacity this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c); //--- Depending on the selected form style, draw the corresponding form frame and the outer bounding frame 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()); } //+------------------------------------------------------------------+
In the method updating the object coordinates, we have previously looped through all objects bound to the form and shifted their coordinates following the current object coordinates. Now we declared the new MoveDependentObj() method the loop is to be arranged in.
Therefore, let's edit the method updating the element coordinates:
//+------------------------------------------------------------------+ //| Update the coordinate elements | //+------------------------------------------------------------------+ 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 the element is not movable and is a base object, leave if(!this.Movable() && base==NULL) return false; //--- If the object has a shadow and we failed to set new coordinate values to the properties of the shadow object, return '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; } //--- If failed to set new values into graphical object properties, return 'false' if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- Shift all bound objects if(!this.MoveDependentObj(x,y,false)) return false; //--- If the update flag is set and this is a base object, redraw the chart. if(redraw && main==NULL) ::ChartRedraw(this.ChartID()); //--- Return 'true' return true; } //+------------------------------------------------------------------+
First, we add getting the pointer to the main object of the entire bound objects hierarchy. This is exactly the object whose shift leads to the shift of all other elements related to it (and to each other).
When moving the shadow object, we now use the graphical element object methods for receiving the shadow shift relative to the object casting it.
Instead of the loop, we now call the new method MoveDependentObj() considered below.
In the end, we now make sure that this is the main object of the entire chain of the related objects hierarchy, rather than the base object of just one of the hierarchy chains.
The method updating the coordinates of bound objects:
//+------------------------------------------------------------------+ //| Update coordinates of bound objects | //+------------------------------------------------------------------+ bool CForm::MoveDependentObj(const int x,const int y,const bool redraw=false) { //--- In the loop by all bound objects, for(int i=0;i<this.m_list_elements.Total();i++) { //--- get the next object and shift it 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; } //+------------------------------------------------------------------+
Here, in the loop by all bound objects, get the next object and call the object's Move() method. Accordingly, all Move() methods are also called in the object for all objects related to it. This causes relocation of the entire hierarchy of connections of all objects attached to the relocated one.
Now let's improve the class of the Panel WinForms object in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh.
In the private section of the class, declare the pointer to the underlay object, the variables for storing initial coordinates and the newly created panel size and the methods for creating the overlay and binding the element to the container:
//+------------------------------------------------------------------+ //| Panel object class of WForms controls | //+------------------------------------------------------------------+ class CPanel : public CForm { private: CGCnvElement *m_underlay; // Underlay for placing elements color m_fore_color; // Default text color for all panel objects ENUM_FW_TYPE m_bold_type; // Font width type ENUM_FRAME_STYLE m_border_style; // Panel frame style bool m_autoscroll; // Auto scrollbar flag int m_autoscroll_margin[2]; // Array of fields around the control during an auto scroll bool m_autosize; // Flag of the element auto resizing depending on the content ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode; // Mode of the element auto resizing depending on the content ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode; // Mode of binding element borders to the container int m_margin[4]; // Array of gaps of all sides between the fields of the current and adjacent controls int m_padding[4]; // Array of gaps of all sides inside controls int m_init_x; // Newly created panel X coordinate int m_init_y; // Newly created panel Y coordinate int m_init_w; // Newly created panel width int m_init_h; // Newly created panel height //--- Return the font flags uint GetFontFlags(void); //--- Create a new graphical object 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); //--- Return the initial coordinates of a bound object virtual void GetCoords(int &x,int &y); //--- Create the underlay object bool CreateUnderlayObj(void); //--- Bind the element to the container bool SetDockingToContainer(void); protected:
In the protected section of the class, set the methods for handling underlay object coordinates:
protected: //--- Set (1) X, (2) Y coordinate, (3) width, (4) height and (5) all underlay parameters 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); //--- Return the underlay (1) X, (2) Y coordinate, (3) width and (4) height 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); } //--- Return the underlay (1) X and (2) Y coordinate relative to the panel 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:
In the public section of the class, write the method returning the pointer to the underlay object and declare the virtual method moving the panel object:
public: //--- Return the underlay CGCnvElement *GetUnderlay(void) { return this.m_underlay; } //--- Update the coordinates (shift the canvas) virtual bool Move(const int x,const int y,const bool redraw=false); //--- (1) Set and (2) return the default text color of all panel objects void ForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; }
Let's improve some public methods.
The method setting the mode of binding the element borders to the container:
//--- (1) Set and (2) return the mode of binding element borders to the container void DockMode(const ENUM_CANV_ELEMENT_DOCK_MODE mode) { if(m_dock_mode==mode) return; this.m_dock_mode=mode; this.SetDockingToContainer(); }
Previously, the method simply set the value passed to it to the corresponding class variable.
Now, I will not only set the value to the variable, but also immediately attach the panel to the desired edges of its container using the SetDockingToContainer() method described below.
Improve the methods setting the gap to the left, top, right and bottom inside the control in the same way:
//--- Set the gap (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides inside the control 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) { //--- Set the underlay shift along the X axis this.m_underlay.SetCoordXRelative(this.PaddingLeft()); //--- Set the X coordinate and the underlay width 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) { //--- Set the underlay shift along the Y axis this.m_underlay.SetCoordYRelative(this.PaddingTop()); //--- Set the Y coordinate and underlay height 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) { //--- Set the underlay width 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) { //--- Set the underlay height 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); }
In addition to setting the values, passed to the method, to the appropriate variables, we immediately change these properties for the underlay object.
The methods setting the form frame width to the left, top, right and below the control are handled the same way:
//--- Set the width of the form frame (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides of the control 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); }
Now, as soon as the frame width is changed on any side of the panel, the appropriate underlay object properties are changed so that the underlay always either fits into the panel frame (if the panel side Padding is less than the frame width on the same side) or corresponds to the Padding value of the panel side.
Remove two redundant constructors from the listing — their declaration and implementation outside the class body:
//--- Constructors 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)
These constructors have turned out to be unnecessary.
In the parametric constructor, add initialization of the panel object properties not done before and of the new variables:
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; } //--- Destructor ~CPanel(); }; //+------------------------------------------------------------------+
Initialize the same variables in the constructor specifying chart and subwindow ID.
The method returning the initial coordinates of the bound object will now return the coordinates relative to the underlay object coordinates, rather than relative to the panel itself and its frame width as it was before:
//+------------------------------------------------------------------+ //| Return the initial coordinates of a bound object | //+------------------------------------------------------------------+ void CPanel::GetCoords(int &x,int &y) { x=this.m_underlay.CoordX()+x; y=this.m_underlay.CoordY()+y; } //+------------------------------------------------------------------+
The method creating the underlay object:
//+------------------------------------------------------------------+ //| Create the underlay object | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+
Here we create a new graphical element object with coordinates and size calculated relative to the Padding values of all the panel sides, so that the underlay object accurately fits the area limited by the Padding values of all the panel sides.
If failed to create the object, inform of that and return false.
If failed to add the newly created object to the panel object list, inform of that, remove the newly created object and return false.
If successful, return the method for setting all parameters of the created underlay and return true.
The method setting all underlay parameters:
//+------------------------------------------------------------------+ //| Set all underlay parameters | //+------------------------------------------------------------------+ bool CPanel::SetUnderlayParams(void) { //--- Set the underlay shift values to the variables by X and Y axes this.m_underlay.SetCoordXRelative(this.PaddingLeft()); this.m_underlay.SetCoordYRelative(this.PaddingTop()); //--- Set the underlay coordinates and size 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; } //+------------------------------------------------------------------+
Here all is simple: set the underlay coordinate shifts relative to the panel coordinates, as well as underlay object coordinates and size.
The method updating the element coordinates:
//+------------------------------------------------------------------+ //| Update the coordinate elements | //+------------------------------------------------------------------+ 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; //--- Get the pointers to the base and main objects in the bound objects hierarchy, as well as the shadow object CGCnvElement *base=this.GetBase(); CGCnvElement *main=this.GetMain(); CShadowObj *shadow=this.GetShadowObj(); //--- If the element is not movable and is a base object, leave if(!this.Movable() && main==NULL) return false; //--- If the object has a shadow and we failed to set new coordinate values to the properties of the shadow object, return '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; } //--- If failed to set new values into graphical object properties, return 'false' if(!this.SetCoordX(x) || !this.SetCoordY(y)) return false; //--- Shift all bound objects if(!this.MoveDependentObj(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative(),false)) return false; //--- If the update flag is set and this is the hierarchy main object, redraw the chart. if(redraw && main==NULL) ::ChartRedraw(this.ChartID()); //--- Return 'true' return true; } //+------------------------------------------------------------------+
The method logic has been described in the code comments in detail. Here we check the object movability. If it is not movable, leave the method. Next, shift the shadow and the object itself. After that, call the method of shifting all bound objects of its hierarchy. Upon completion, make sure that this is the main object of the bound objects hierarchy and update the chart.
The method binding an element to a container:
//+------------------------------------------------------------------+ //| Bind the element to the container | //+------------------------------------------------------------------+ bool CPanel::SetDockingToContainer(void) { //--- Get the pointer to the pnael object the object is bound to CPanel *base=this.GetBase(); if(base==NULL) return false; //--- Declare the variables and get the base object coordinates abd size to it int x=base.GetCoordXUnderlay(); int y=base.GetCoordYUnderlay(); int w=base.GetWidthUnderlay(); int h=base.GetHeightUnderlay(); //--- Depending on the specified mode of binding to a container, move the object to the necessary base object edges switch(this.DockMode()) { //--- Attach to the top and stretch along the container width 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; //--- Attach to the bottom and stretch along the container width 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; //--- Attach to the left and stretch along the container height 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; //--- Attach to the right and stretch along the container height 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; //--- Stretch along the entire container width and height 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; //--- Attached to the specified coordinates, size does not change 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; } //+------------------------------------------------------------------+
The method logic is also described in the code comments. Depending on how an object is bound to a container, calculate the necessary coordinates and size, as well as set the object to the new coordinates. If the object is not bound to the container sides, get its initial coordinates and size.
The method is always called when setting a new value for the object DockMode property.
Now let's adjust the methods in the collection class of graphical elements in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
Since we now have the ability to simultaneously set both the single color of the form background and the colors of its gradient fill, replace specifying the color using the
obj.SetColorBackground(clr[0]);
with SetColorsBackground() in all methods of creating form objects with the gradient filling:
//--- Create a graphical object form object on canvas on a specified chart and subwindow with the vertical gradient filling 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(); }
Now the method receives the array itself rather than the first color from the color array.
Such changes have already been made in all methods for creating form objects.
Let's make similar improvements in the methods for creating panel objects:
//--- Create a WinForms Panel object graphical object on canvas on a specified chart and subwindow with the vertical gradient filling 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); //--- Draw the shadow drawing flag obj.SetShadow(shadow); if(shadow) { //--- Calculate the shadow color as the chart background color converted to the monochrome one //--- and darken the monochrome color by 20 units color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-100),-20); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to the default value, while the blur radius is equal to 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(); }
Here we also pass the gradient filling color array to the object properties. Besides, we set the embossed frame type to the object properties and use the type when drawing the frame. Previously, I simply drew the frame with the type passed to the method without setting the frame type in the object itself. This caused skipping the frame when redrawing the object since the default property indicating the frame absence remained. Now I have fixed this.
Such changes have already been made to all methods for creating panels with the gradient filling. You can find them in the files attached below.
Let's temporarily set the code for displaying texts on objects bound to the panel, so that we can make sure IDs and ZOrder properties are correctly assigned to the panel.
Add the following code blocks to the method setting ZOrder to the specified element and adjusting it in all other elements:
//+------------------------------------------------------------------+ //| Set ZOrde to the specified element | //| and adjust it in other elements | //+------------------------------------------------------------------+ bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj) { //--- Get the maximum ZOrder of all graphical elements long max=this.GetZOrderMax(); //--- If an invalid pointer to the object has been passed or the maximum ZOrder has not been received, return 'false' if(obj==NULL || max<0) return false; //--- Declare the variable for storing the method result bool res=true; //--- If the maximum ZOrder is zero, ZOrder is equal to 1, //--- if the maximum ZOrder is less than (the total number of graphical elements)-1, ZOrder will exceed it by 1, //--- otherwise, ZOrder will be equal to (the total number of graphical elements)-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); //--- If failed to set ZOrder for an object passed to the method, return 'false' if(!obj.SetZorder(value,false)) return false; //--- Temporarily declare a form object for drawing a text for visually displaying its ZOrder CForm *form=obj; //--- and draw a text specifying ZOrder on the form 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); //--- Temporarily (for the test purpose), if the element is a form or higher 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()); } } //--- Sort the list of graphical elements by an element ID this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID); //--- Get the list of graphical elements without an object whose ID is equal to the ID of the object passed to the method CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID,obj.ID(),NO_EQUAL); //--- If failed to obtain the list and the list size exceeds one, //--- which indicates the presence of other objects in it in addition to the one sorted by ID, return 'false' if(list==NULL && this.m_list_all_canv_elm_obj.Total()>1) return false; //--- In the loop by the obtained list of remaining graphical element objects for(int i=0;i<list.Total();i++) { //--- get the next object CGCnvElement *elm=list.At(i); //--- If failed to get the object or if this is a control form for managing pivot points of an extended standard graphical object //--- or, if the object's ZOrder is zero, skip the object since there is no need in changing its ZOrder as it is the bottom one if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0) continue; //--- If failed to set the object's ZOrder to 1 less than it already is (decreasing ZOrder by 1), add 'false' to the 'res' value if(!elm.SetZorder(elm.Zorder()-1,false)) res &=false; //--- Temporarily (for the test purpose), if the element is a form or higher if(elm.Type()>=OBJECT_DE_TYPE_GFORM) { //--- assign the pointer to the element for the form and draw a text specifying ZOrder on the form 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()); } } } //--- Upon the loop completion, return the result set in 'res' return res; } //+------------------------------------------------------------------+
The code allows us to find an object bound to the panel and display its ID and ZOrder value.
The text output is not always triggered on time but this is of no importance now as this functionality is needed only once. After that, I will remove the codes from the method.
Now we are all set for the test.
Test
Let's take the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part104\ as TestDoEasyPart104.mq5.
In the previous EA, I have already created one panel, which in turn contains a few other panels. Now I am going to create a single panel object inside the panel and assign the keys to bind it to the edges of the main panel. By pressing the keys on the keyboard, we will set all possible types of binding the dependent panel to the sides of the main one. The Padding value equal to 10 is assigned to the main panel to let the indent from the edges of the panel be visible, so that we can see how padding works when positioning one object inside another.
Set the following keys:
- W — bind to the top edge,
- A — bind to the left edge,
- D — bind to the right edge,
- X — bind to the bottom edge,
- S — fill,
- Z — reset binding and restore the original size and coordinates.
Assign the key codes in the global area:
//+------------------------------------------------------------------+ //| TestDoEasyPart104.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <DoEasy\Engine.mqh> //--- defines #define FORMS_TOTAL (3) // Number of created forms #define START_X (4) // Initial X coordinate of the shape #define START_Y (4) // Initial Y coordinate of the shape #define KEY_LEFT (65) // (A) Left #define KEY_RIGHT (68) // (D) Right #define KEY_UP (87) // (W) Up #define KEY_DOWN (88) // (X) Down #define KEY_CENTER (83) // (S) Center #define KEY_ORIGIN (90) // (Z) Default //--- 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[]; //+------------------------------------------------------------------+
In the EA's OnInit(), create all the objects (this was done before) and create a panel inside another one:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set EA global variables ArrayResize(array_clr,2); // Array of gradient filling colors array_clr[0]=C'26,100,128'; // Original ≈Dark-azure color array_clr[1]=C'35,133,169'; // Lightened original color //--- Create the array with the current symbol and set it to be used in the library string array[1]={Symbol()}; engine.SetUsedSymbols(array); //--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions //--- Create form objects 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; //--- Set ZOrder to zero, display the text describing the gradient type and update the form //--- Text parameters: the text coordinates and the anchor point in the form center //--- Create a new text animation frame with the ID of 0 and display the text on the form 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); } //--- Create four graphical elements CGCnvElement *elm=NULL; array_clr[0]=C'0x65,0xA4,0xA9'; array_clr[1]=C'0x48,0x75,0xA2'; //--- Vertical gradient 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(); } //--- Vertical cyclic gradient 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(); } //--- Horizontal gradient 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(); } //--- Horizontal cyclic gradient 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(); } //--- Create WinForms Panel object 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) { //--- Set the Padding value to 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()); //--- In the loop, create N bound panel objects (a single panel) CPanel *obj=NULL; for(int i=0;i<1;i++) { //--- create the panel object with coordinates along the X axis in the center and 10 along the Y axis, the width of 80 and the height of 50 pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_PANEL,pnl,pnl.GetUnderlay().Width()/2-40,10,80,50,C'0xCD,0xDA,0xD7',200,true); //--- To control the creation of bound objects, //--- get the pointer to the bound object by the loop index obj=pnl.GetElement(i); //--- take the pointer to the base object from the obtained object //--- and display the name of a created bound object and the name of its base object in the journal 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) { //--- Display the ID and zorder on the newly created panel 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()); //--- Set the frame color, active panel area and draw the frame 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); } //+------------------------------------------------------------------+
In the OnChartEvent() handler, add the following keystroke handling code:
//--- If a key is pressed 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); }
Here we get the panel object by its ID (we know for sure that its ID is 7), get the very first (and only) object from its list of bound objects and set DockMode for the obtained panel object depending on the code of a pressed key.
Compile the EA, launch it on the chart and press some keyboard keys:
As we can see, when pressing various keys and setting the appropriate binding methods, the panel is positioned inside its container correctly. Pressing Z restores its initial size and coordinates. While doing all this, the panel does not stick directly to the edges of the container, but is located at the Padding distance from the edges of the main panel. When relocating the main panel, the panel attached to it is also relocated correctly using newly set coordinates depending on the current binding mode.
What's next?
In the next article, I will continue my work on WinForms objects.
*Previous articles within the series:
DoEasy. Controls (Part 1): First steps
DoEasy. Controls (Part 2): Working on the CPanel class
DoEasy. Controls (Part 3): Creating bound controls
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10756





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use