DoEasy. Controls (Part 24): Hint auxiliary WinForms object

Artyom Trishkin | 14 December, 2022

Contents


Concept

In the library, many graphical elements are related to each other in one way or another. Each control has a pointer to its base object and the main parent object of the entire hierarchy of related objects. The current implementation of specifying these objects for a newly created graphical element contains flaws causing the element to "lose" its parent objects. We specify them after creating the object. However, this does not always lead to a result. Finding the place where the error lies is quite complicated. Therefore, I decided to change the logic of specifying parent objects in the relationship hierarchy. I will add pointers to parent objects into all constructors of all graphical elements. Thus, we will specify them directly at the moment of creating a graphical object, which will save us from having to specify parent objects each time after creating the next bound control. For those controls that are independent (not created as objects attached to any element), I will pass the NULL value as pointers to parent objects.

Such a rework will save us from searching for a bug, due to which not all such objects are "aware" of their parents, and will simplify writing and debugging new library controls in the future.

In addition to finalizing the constructors of graphical element objects, I will create several new WinForms objects. When working with controls in Windows applications, you may notice how the mouse cursor changes appearance when it enters the area of an object. The change indicates that interaction with this region may, for example, result in a change in the object appearance. It may also indicate the ability to perform some action in this area, for example, use the drag-n-drop functionality.

Unfortunately, the MQL5 language does not allow changing the appearance of the mouse cursor. But somehow we need to inform library users of the possible interaction with controls. My solution is as follows: where it is possible to interact with the mouse cursor, we will display auxiliary graphical objects. The appearance of these objects will indicate the possible actions that can be performed in this area of the control.

I will call such auxiliary elements hint objects. For all hint objects, I will create a base object with basic functionality. It will be used to make specific objects - each with its own purpose.

In the current article, I will create a base hint object and its derivatives - objects that indicate the possibility of shifting the separator up, down, left and right. As soon as the mouse cursor is over the separator area in the SplitContainer control, such objects will appear next to the cursor, indicating that the separator can be moved left-right or up-down.

In the future, hint objects created today and later will be used as part of new controls. Alternatively, using such objects, we can point to new functionality that will be added to already created controls, for example, to indicate the possibility of changing the size of a graphical object.


Improving library classes

Colors and size should be set for a hint object. Let's define them in \MQL5\Include\DoEasy\Defines.mqh:

#define CLR_DEF_CONTROL_SPLIT_CONTAINER_BACK_COLOR    (C'0xF0,0xF0,0xF0')  // SplitContainer control background color
#define CLR_DEF_CONTROL_SPLIT_CONTAINER_MOUSE_DOWN    (C'0xF0,0xF0,0xF0')  // Color of SplitContainer control background when clicking on the control
#define CLR_DEF_CONTROL_SPLIT_CONTAINER_MOUSE_OVER    (C'0xF0,0xF0,0xF0')  // Color of SplitContainer control background when hovering the mouse over the control
#define CLR_DEF_CONTROL_SPLIT_CONTAINER_BORDER_COLOR  (C'0x65,0x65,0x65')  // SplitContainer control frame color

#define CLR_DEF_CONTROL_HINT_BACK_COLOR               (C'0xFF,0xFF,0xE1')  // Hint control background color
#define CLR_DEF_CONTROL_HINT_BORDER_COLOR             (C'0x76,0x76,0x76')  // Hint control frame color
#define CLR_DEF_CONTROL_HINT_FORE_COLOR               (C'0x5A,0x5A,0x5A')  // Hint control text color

#define DEF_CONTROL_LIST_MARGIN_X      (1)                        // Gap between columns in ListBox controls
#define DEF_CONTROL_LIST_MARGIN_Y      (0)                        // Gap between rows in ListBox controls

#define DEF_FONT                       ("Calibri")                // Default font
#define DEF_FONT_SIZE                  (8)                        // Default font size
#define DEF_CHECK_SIZE                 (12)                       // Verification flag default size
#define DEF_ARROW_BUTTON_SIZE          (15)                       // Default arrow button size
#define OUTER_AREA_SIZE                (16)                       // Size of one side of the outer area around the form workspace
#define DEF_FRAME_WIDTH_SIZE           (3)                        // Default form/panel/window frame width
#define DEF_HINT_ICON_SIZE             (11)                       // Hint object side size
//--- Graphical object parameters
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Maximum value of an ID of a graphical object belonging to a program
#define CTRL_POINT_RADIUS              (5)                        // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_POINT_COLOR               (clrDodgerBlue)            // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_FORM_SIZE                 (40)                       // Size of the control point form for managing graphical object pivot points
//+------------------------------------------------------------------+


Add new object types we are going to create here to the list of graphical element types:

//+------------------------------------------------------------------+
//| The list of graphical element types                              |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Standard graphical object
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Extended standard graphical object

   //---...
   //---...

   GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,            // Windows Forms CheckedListBox
   GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,             // Windows Forms ButtonListBox
   //--- Auxiliary elements of WinForms objects
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,               // Windows Forms ListBoxItem
   GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,                  // Windows Forms TabHeader
   GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,                   // Windows Forms TabField
   GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,       // Windows Forms SplitContainerPanel
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON,                // Windows Forms ArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,             // Windows Forms UpArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,           // Windows Forms DownArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT,           // Windows Forms LeftArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT,          // Windows Forms RightArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,        // Windows Forms UpDownArrowButtonsBox
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,        // Windows Forms LeftRightArrowButtonsBox
   GRAPH_ELEMENT_TYPE_WF_SPLITTER,                    // Windows Forms Splitter
   GRAPH_ELEMENT_TYPE_WF_HINT_BASE,                   // Windows Forms HintBase
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,              // Windows Forms HintMoveLeft
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,             // Windows Forms HintMoveRight
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,                // Windows Forms HintMoveUp
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,              // Windows Forms HintMoveDown
  };
//+------------------------------------------------------------------+


In \MQL5\Include\DoEasy\Data.mqh, add the library new message indices:

   MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,    // UpDownArrowBox control
   MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,    // LeftRightArrowBox control
   MSG_GRAPH_ELEMENT_TYPE_WF_HINT_BASE,               // HintBase control
   MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,          // HintMoveLeft control
   MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,         // HintMoveLeft control
   MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,            // HintMoveLeft control
   MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,          // HintMoveLeft control
   MSG_GRAPH_OBJ_BELONG_PROGRAM,                      // Graphical object belongs to a program
   MSG_GRAPH_OBJ_BELONG_NO_PROGRAM,                   // Graphical object does not belong to a program

and the message texts corresponding to the newly added indices:

   {"Элемент управления \"UpDownArrowBox\"","Control element \"UpDownArrowBox\""},
   {"Элемент управления \"LeftRightArrowBox\"","Control element \"LeftRightArrowBox\""},
   {"Элемент управления \"HintBase\"","Control element \"HintBase\""},
   {"Элемент управления \"HintMoveLeft\"","Control element \"HintMoveLeft\""},
   {"Элемент управления \"HintMoveRight\"","Control element \"HintMoveRight\""},
   {"Элемент управления \"HintMoveUp\"","Control element \"HintMoveUp\""},
   {"Элемент управления \"HintMoveDown\"","Control element \"HintMoveDown\""},
   {"Графический объект принадлежит программе","The graphic object belongs to the program"},
   {"Графический объект не принадлежит программе","The graphic object does not belong to the program"},


Now we can display descriptions of new objects that will be created here. The method in \MQL5\Include\DoEasy\Objects\Graph\GBaseObj.mqh allows us to display descriptions of graphical objects. Add the return of the necessary string by the element type in that method:

//+------------------------------------------------------------------+
//| Return the description of the graphical element type             |
//+------------------------------------------------------------------+
string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return
     (
      type==GRAPH_ELEMENT_TYPE_STANDARD                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)                 :
      type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)        :
      type==GRAPH_ELEMENT_TYPE_ELEMENT                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT)                  :
      type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ)               :
      type==GRAPH_ELEMENT_TYPE_FORM                      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM)                     :
      type==GRAPH_ELEMENT_TYPE_WINDOW                    ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW)                   :
      //--- WinForms
      type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY)              :
      type==GRAPH_ELEMENT_TYPE_WF_BASE                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE)                  :
      //--- Containers
      type==GRAPH_ELEMENT_TYPE_WF_CONTAINER              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER)             :
      type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_PANEL                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL)                 :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)           :
      type==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER)       :
      //--- Standard controls
      type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE)           :
      type==GRAPH_ELEMENT_TYPE_WF_LABEL                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL)                 :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON)           :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON)                :
      type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX)     :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM          ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM)         :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX       ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX)      :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX)       :
      //--- Auxiliary control objects
      type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER             ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER)            :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD)             :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON)          :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP)       :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN)     :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT)     :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT     ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT)    :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL) :
      type==GRAPH_ELEMENT_TYPE_WF_SPLITTER               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLITTER)              :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_BASE              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_BASE)             :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT)        :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT)       :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP)          :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN)        :
      "Unknown"
     );
  }  
//+------------------------------------------------------------------+


I will implement the main changes in specifying and receiving parent objects in the class of the graphical element object. The pointers to the base and main objects of the relationship hierarchy of the created graphical element are to be sent to the class constructor.

In \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, remove the methods for setting parent objects (we will now pass them directly to the class constructor) leaving only methods for returning pointers. IsBase() method is renamed into IsDependent():

//--- (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;             }
   
//--- Return the pointer to the parent element within related objects of the current group
   CGCnvElement     *GetBase(void)                                                     { return this.m_element_base;                }
//--- Return the pointer to the parent element within all groups of related objects
   CGCnvElement     *GetMain(void)     { return(this.m_element_main==NULL ? this.GetObject() : this.m_element_main);                }

//--- Return the flag indicating that the object is (1) main, (2) base
   bool              IsMain(void)                                                      { return this.m_element_main==NULL;          }
   bool              IsDependent(void)                                                 { return this.m_element_base!=NULL;          }
//--- Get the (1) main and (2) base object ID
   int               GetMainID(void)
                       {
                        if(this.IsMain())
                           return this.ID();
                        CGCnvElement *main=this.GetMain();
                        return(main!=NULL ? main.ID() : WRONG_VALUE);
                       }
   int               GetBaseID(void)
                       {
                        if(!this.IsDependent())
                           return this.ID();
                        CGCnvElement *base=this.GetBase();
                        return(base!=NULL ? base.ID() : WRONG_VALUE);
                       }

Since the name of the IsBase method directly indicates that it checks whether this is the base object for some other object, this is not entirely correct. The point is that if the value of the m_element_base variable equals to NULL, then this indicates that the object does not have a base one - it is not attached to any other object. However, it can act as a base object for others (if they are created inside it as attached objects) or not act like one if no graphical element is attached to it.

Thus, we can see that stating a question (Is Base) in such a way is incorrect. But we can find out if the object is dependent (attached to the parent one) — if m_element_base is NULL, then the object is not attached to any other, i.e. it is independent, otherwise the object is dependent on its parent base object. Therefore, I have renamed the IsBase method to IsDependent and changed the logic of defining the object dependence on another — the flag indicating that m_element_base is not equal to NULL is returned.
The checks for the base object have also been changed accordingly. Now the object independence flag is checked like in this method:

   int               GetBaseID(void)
                       {
                        if(!this.IsDependent())
                           return this.ID();
                        CGCnvElement *base=this.GetBase();
                        return(base!=NULL ? base.ID() : WRONG_VALUE);
                       }

If the object is not bound to any other, then its ID is returned. Otherwise, the ID of the base object is returned.


In the declaration of constructors (protected and public parametric one), I will write new formal variables through which we will pass the pointers to the mainand base objects the created object is bound to:

protected:
//--- Protected constructor
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  CGCnvElement *main_obj,CGCnvElement *base_obj,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  descript,
                                  const int     x,
                                  const int     y,
                                  const int     w,
                                  const int     h);
public:
//--- (1) Set and (2) return the X coordinate shift relative to the base object
   void              SetCoordXRelative(const int value)                                { this.m_shift_coord_x=value;                }
   int               CoordXRelative(void)                                        const { return this.m_shift_coord_x;               }
//--- (1) Set and (2) return the Y coordinate shift relative to the base object
   void              SetCoordYRelative(const int value)                                { this.m_shift_coord_y=value;                }
   int               CoordYRelative(void)                                        const { return this.m_shift_coord_y;               }
   
//--- Event handler
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Parametric constructor
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  CGCnvElement *main_obj,CGCnvElement *base_obj,
                                  const int     element_id,
                                  const int     element_num,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  descript,
                                  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);
//--- Default constructor
                     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;
                        }
//--- Destructor
                    ~CGCnvElement()
                        { this.m_canvas.Destroy();             }
     
//+------------------------------------------------------------------+

Now, when creating an object, we will definitely need to pass the pointers to its two parent objects to the constructor. If the object is created as independent, then instead of pointers we will enter NULL. This will completely save us from additional actions for specifying these objects after the creation of a new graphical element.

Previously, we specified default values for parent objects in the constructors:

   this.m_element_main=NULL;
   this.m_element_base=NULL;

Now we will again add the values passed in formal parameters:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           CGCnvElement *main_obj,CGCnvElement *base_obj,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   descript,
                           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.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=main_obj;
   this.m_element_base=base_obj;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   //---...
   //---...

Now each created object will immediately "know" its parent objects. If this graphical element is not attached to any other, then both variables will have the NULL values indicating that the object itself is main and independent.


The method that crops the image outlined by the calculated rectangular visibility area will now feature the check that an object is independent of the parent:

//+------------------------------------------------------------------+
//| Crop the image outlined by the calculated                        |
//| rectangular visibility scope                                     |
//+------------------------------------------------------------------+
void CGCnvElement::Crop(void)
  {
//--- Get the pointer to the base object
   CGCnvElement *base=this.GetBase();
//--- If the object does not have a base object it is attached to, then there is no need to crop the hidden areas - leave
   if(!this.IsDependent())
      return;
//--- Set the initial coordinates and size of the visibility scope to the entire object
//---...
//---...


In the \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh file of the shadow object class, add passing the pointers to the main and base objects:

//--- Draw the object shadow form
   void              DrawShadowFigureRect(const int w,const int h);

public:
//--- Constructor indicating the main and base objects, chart ID and subwindow
                     CShadowObj(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                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


While implementing the constructor, pass the pointers to the main and base objects to the parent object:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CShadowObj::CShadowObj(CGCnvElement *main_obj,CGCnvElement *base_obj,
                       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,main_obj,base_obj,chart_id,subwindow,name,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GSHADOW; 
   CGCnvElement::SetBackgroundColor(clrNONE,true);
   CGCnvElement::SetOpacity(0);
   CGCnvElement::SetActive(false);
   this.m_opacity=CLR_DEF_SHADOW_OPACITY;
   this.m_blur=DEF_SHADOW_BLUR;
   color gray=CGCnvElement::ChangeColorSaturation(this.ChartBackgroundColor(),-100);
   this.m_color=CGCnvElement::ChangeColorLightness(gray,-50);
   this.m_shadow=false;
   this.SetVisibleFlag(false,false);
   CGCnvElement::Erase();
  }
//+------------------------------------------------------------------+

When creating a shadow object for any object, the shadow object constructor receives the pointers to the object the shadow is created from (or for). Accordingly, in this constructor, the pointers to the main and base objects will be passed to the appropriate variables of the CGCnvElement base object constructor. Thus, we pass the pointers in a chain to the parent object of the entire inheritance hierarchy through the constructors of all child classes. This way we always ensure that the main and base objects are exactly specified for any control we create immediately upon its creation.


In the \MQL5\Include\DoEasy\Objects\Graph\Form.mqh file of the form object class in all the constructors, add passing the pointers to the main and base objects:

protected:
//--- Protected constructor with object type, chart ID and subwindow
                     CForm(const ENUM_GRAPH_ELEMENT_TYPE type,
                           CGCnvElement *main_obj,CGCnvElement *base_obj,
                           const long chart_id,
                           const int subwindow,
                           const string descript,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
public:                     
//--- Constructors
                     CForm(CGCnvElement *main_obj,CGCnvElement *base_obj,
                           const long chart_id,
                           const int subwindow,
                           const string descript,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm() { this.m_type=OBJECT_DE_TYPE_GFORM; this.Initialize(); }
//--- Destructor
                    ~CForm();

...

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CForm::CForm(const ENUM_GRAPH_ELEMENT_TYPE type,
             CGCnvElement *main_obj,CGCnvElement *base_obj,
             const long chart_id,
             const int subwindow,
             const string descript,
             const int x,
             const int y,
             const int w,
             const int h) : CGCnvElement(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GFORM; 
   this.Initialize();
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.SetWidthInit(w);
   this.SetHeightInit(h);
  }
//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CForm::CForm(CGCnvElement *main_obj,CGCnvElement *base_obj,
             const long chart_id,
             const int subwindow,
             const string descript,
             const int x,
             const int y,
             const int w,
             const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_FORM,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GFORM; 
   this.Initialize();
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.SetWidthInit(w);
   this.SetHeightInit(h);
  }
//+------------------------------------------------------------------+


In the method creating a new graphical object, add passing the pointers to the main and base objects:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int obj_num,
                                      const string descript,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool movable,
                                      const bool activity)
  {
   CGCnvElement *element=NULL;
   //--- Depending on the created object type,
   switch(type)
     {
      //--- create a graphical element object
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.GetMain(),this.GetObject(),this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity);
        break;
      //--- create a form object
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(type,this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   element.SetMovable(movable);
   element.SetCoordXRelative(element.CoordX()-this.CoordX());
   element.SetCoordYRelative(element.CoordY()-this.CoordY());
   return element;
  }
//+------------------------------------------------------------------+

Since an element bound to the base object is created in these strings, the value returned from the GetMain() method is passed as a pointer to the main object, while the value returned by the GetObject() method is passed as a pointer to the base one. The pointer to the main object was already registered in this object when it was created. It is this pointer that we send to the constructor of the newly created bound object. Thus, the main object for the entire hierarchy of the link chain will always remain the same - the very first one in this chain, while the pointer to this object is passed as the pointer to the base object. Therefore, the pointer to the base object for the newly created bound element should point to this object.


Remove the indication of the main and base objects from the CreateAndAddNewElement() and CreateNewElement() methods, since these methods have already been removed from the graphical element object class, and now pointers to the main and base objects are passed in the class constructors:

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//| and add it to the list of bound objects                          |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            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 NULL;
     }
//--- Specify the element index in the list
   int num=this.m_list_elements.Total();
//--- Create a description of the default graphical element
   string descript=TypeGraphElementAsString(element_type);
//--- 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,descript,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      .return NULL;
//--- and add it to the list of bound graphical elements
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      delete obj;
      .return NULL;
     }
//--- Set the minimum properties for a bound graphical element
   obj.SetBackgroundColor(colour,true);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
   obj.SetID(this.GetMaxIDAll()+1);
   obj.SetNumber(num);
   obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
   obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(obj.CoordXRelative());
   obj.SetCoordYRelativeInit(obj.CoordYRelative());
   obj.SetVisibleFlag(this.IsVisible(),false);
   obj.SetActive(this.Active());
   obj.SetEnabled(this.Enabled());
   return obj;
  }
//+------------------------------------------------------------------+

...

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//+------------------------------------------------------------------+
bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             const int x,
                             const int y,
                             const int w,
                             const int h,
                             const color colour,
                             const uchar opacity,
                             const bool activity,
                             const bool redraw)
  {
//--- Create a new graphical element
   CGCnvElement *obj=this.CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
//--- If the object has been created, draw the added object and return 'true'
   if(obj==NULL)
      return false;
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetBase());
   obj.Erase(colour,opacity,redraw);
   return true;
  }
//+------------------------------------------------------------------+


In the method that creates the shadow object, pass the pointers to the main and base objects to the shadow object class constructor:

//+------------------------------------------------------------------+
//| Create the shadow object                                         |
//+------------------------------------------------------------------+
void CForm::CreateShadowObj(const color colour,const uchar opacity)
  {
//--- If the shadow flag is disabled or the shadow object already exists, exit
   if(!this.m_shadow || this.m_shadow_obj!=NULL)
      return;
//--- Calculate the shadow object coordinates according to the offset from the top and left
   int x=this.CoordX()-OUTER_AREA_SIZE;
   int y=this.CoordY()-OUTER_AREA_SIZE;
//--- Calculate the width and height in accordance with the top, bottom, left and right offsets
   int w=this.Width()+OUTER_AREA_SIZE*2;
   int h=this.Height()+OUTER_AREA_SIZE*2;
//--- Create a new shadow object and set the pointer to it in the variable
   this.m_shadow_obj=new CShadowObj(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),this.NameObj()+"Shadow",x,y,w,h);
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ));
      return;
     }
   this.m_list_tmp.Add(this.m_shadow_obj);
//--- Set the properties for the created shadow object
   this.m_shadow_obj.SetID(this.ID());
   this.m_shadow_obj.SetNumber(-1);
   this.m_shadow_obj.SetOpacity(opacity);
   this.m_shadow_obj.SetColor(colour);
   this.m_shadow_obj.SetMovable(this.Movable());
   this.m_shadow_obj.SetActive(false);
   this.m_shadow_obj.SetVisibleFlag(false,false);
//--- Move the form object to the foreground
   this.BringToTop();
  }
//+------------------------------------------------------------------+

The logic for passing pointers here is exactly the same as described above - we get and send the main object set in this object, and we specify this object as the base one.

In the method that updates the element coordinates, check the object independence flag and the flag indicating that this is the main object:

//+------------------------------------------------------------------+
//| Update the coordinate elements                                   |
//+------------------------------------------------------------------+
bool CForm::Move(const int x,const int y,const bool redraw=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();
   bool res=true;
//--- If the element is not movable and it is an independent object, leave
   if(!this.IsDependent() && !this.Movable())
      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 && this.m_shadow_obj!=NULL)
     {
      if(!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 the main object, redraw the chart.
   if(this.IsMain() && redraw)
      ::ChartRedraw(this.ChartID());
   //--- Return 'true'
   return true;
  }
//+------------------------------------------------------------------+


In the \MQL5\Include\DoEasy\Objects\Graph\GraphElmControl.mqh file of the graphical element management class, namely in the method creating a form object on the specified chart in a specified subwindow, add specification of the main and base objects, or, to be more precise, indicate their absence:

//+----------------------------------------------------------------------+
//| Create the form object on a specified chart in a specified subwindow |
//+----------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
  {
   CForm *form=new CForm(NULL,NULL,chart_id,wnd,name,x,y,w,h);
   if(form==NULL)
      .return NULL;
   form.SetID(form_id);
   form.SetNumber(0);
   return form;
  }
//+------------------------------------------------------------------+

Here we pass NULL values to the constructor of the form object as pointers to the main and base objects, which means that the created object is not tied to any other and is itself the main one.


Also, specify the NULL values in the initialization string of the constructor class of the form for managing anchor points of a graphical object in \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh:

//+------------------------------------------------------------------+
//| Class of the form for managing pivot points of a graphical object|
//+------------------------------------------------------------------+
class CFormControl : public CForm
  {
private:
   bool              m_drawn;                   // Flag indicating that the pivot point is drawn on the form
   int               m_pivot_point;             // Pivot point managed by the form
public:
//--- (1) Return and (2) set the drawn point flag
   bool              IsControlAlreadyDrawn(void)               const { return this.m_drawn;                       }
   void              SetControlPointDrawnFlag(const bool flag)       { this.m_drawn=flag;                         }
//--- (1) Return and (2) set the pivot point managed by the form
   int               GraphObjPivotPoint(void)                  const { return this.m_pivot_point;                 }
   void              SetGraphObjPivotPoint(const int index)          { this.m_pivot_point=index;                  }
//--- Constructor
                     CFormControl(void)                              { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;  }
                     CFormControl(const long chart_id,const int subwindow,const string name,const int pivot_point,const int x,const int y,const int w,const int h) :
                        CForm(GRAPH_ELEMENT_TYPE_FORM,NULL,NULL,chart_id,subwindow,name,x,y,w,h)
                          {
                           this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;
                           this.m_pivot_point=pivot_point;
                          }
  };
//+------------------------------------------------------------------+


In the \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh file of the base object class of all WinForms library objects, add passing the pointers to the main and base objects to the class constructors:

protected:
//--- Protected constructor with object type, chart ID and subwindow
                     CWinFormBase(const ENUM_GRAPH_ELEMENT_TYPE type,
                                  CGCnvElement *main_obj,CGCnvElement *base_obj,
                                  const long chart_id,
                                  const int subwindow,
                                  const string descript,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h);
public:
//--- Constructors
                     CWinFormBase(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                  const long chart_id,
                                  const int subwindow,
                                  const string descript,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h);
                       
//--- (1) Set and (2) return the default text color of all panel objects

...

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CWinFormBase::CWinFormBase(const ENUM_GRAPH_ELEMENT_TYPE type,
                           CGCnvElement *main_obj,CGCnvElement *base_obj,
                           const long chart_id,
                           const int subwindow,
                           const string descript,
                           const int x,
                           const int y,
                           const int w,
                           const int h) : CForm(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
//--- Initialize all variables
   this.SetText("");
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetForeStateOnColor(this.ForeColor(),true);
   this.SetForeStateOnColorMouseDown(this.ForeColor());
   this.SetForeStateOnColorMouseOver(this.ForeColor());
   this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
   this.SetFontBoldType(FW_TYPE_NORMAL);
   this.SetMarginAll(0);
   this.SetPaddingAll(0);
   this.SetBorderSizeAll(0);
   this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetAutoSize(false,false);
   CForm::SetCoordXInit(x);
   CForm::SetCoordYInit(y);
   CForm::SetWidthInit(w);
   CForm::SetHeightInit(h);
   this.m_shadow=false;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
  }
//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CWinFormBase::CWinFormBase(CGCnvElement *main_obj,CGCnvElement *base_obj,
                           const long chart_id,
                           const int subwindow,
                           const string descript,
                           const int x,
                           const int y,
                           const int w,
                           const int h) : CForm(GRAPH_ELEMENT_TYPE_WF_BASE,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the graphical element and library object types as a base WinForms object
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BASE);
   this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
//--- Initialize all variables
   this.SetText("");
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetForeStateOnColor(this.ForeColor(),true);
   this.SetForeStateOnColorMouseDown(this.ForeColor());
   this.SetForeStateOnColorMouseOver(this.ForeColor());
   this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
   this.SetFontBoldType(FW_TYPE_NORMAL);
   this.SetMarginAll(0);
   this.SetPaddingAll(0);
   this.SetBorderSizeAll(0);
   this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetAutoSize(false,false);
   CForm::SetCoordXInit(x);
   CForm::SetCoordYInit(y);
   CForm::SetWidthInit(w);
   CForm::SetHeightInit(h);
   this.m_shadow=false;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
  }
//+------------------------------------------------------------------+


In the method that redraws the object, change the check that this is the main object:

//+------------------------------------------------------------------+
//| Redraw the object                                                |
//+------------------------------------------------------------------+
void CWinFormBase::Redraw(bool redraw)
  {
//--- If the object type is less than the "Base WinForms object" or the object is not to be displayed, exit
   if(this.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_BASE || !this.Displayed())
      return;
//--- Get the "Shadow" object

//---...
//---...

//--- If the redraw flag is set and if this is the main object the rest are bound to,
//--- redraw the chart to display changes immediately
   if(this.IsMain() && redraw)
      ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

Previously, the check was performed the following way here:

if(redraw && this.GetMain()==NULL)

In essence, this is the same thing, but since there is a method, why not use it?

The above improvements to class constructors and checks that the object is the main or base one have already been performed in absolutely all classes of all WinForms library objects. For new objects, this will be the default behavior. I will not describe such improvements here any further to save the article space.

Headers of class files, in which only constructors for passing pointers to the main and base objects have been improved:

CommonBase.mqh, Button.mqh, ElementsListBox.mqh, RadioButton.mqh, ArrowButton.mqh, ArrowLeftButton.mqh, ArrowRightButton.mqh, ArrowUpButton.mqh, ArrowDownButton.mqh, ListBoxItem.mqh, TabHeader.mqh.

But there are class files where changes have been made to the methods for creating a new graphical object in addition to improved constructors. Here we have an improved passing the pointers to the main and base objects to the constructor of the created object:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CButtonListBox::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                               const int obj_num,
                                               const string descript,
                                               const int x,
                                               const int y,
                                               const int w,
                                               const int h,
                                               const color colour,
                                               const uchar opacity,
                                               const bool movable,
                                               const bool activity)
  {
//--- create the CButton object
   CGCnvElement *element=new CButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
//--- set the object relocation flag and relative coordinates
   element.SetMovable(movable);
   element.SetCoordXRelative(element.CoordX()-this.CoordX());
   element.SetCoordYRelative(element.CoordY()-this.CoordY());
   return element;
  }
//+------------------------------------------------------------------+


The same or similar improvements to the methods that create a new graphical object (in addition to the refinement of the constructors) are made in the files:

ButtonListBox.mqh, CheckedListBox.mqh, ListBox.mqh, ArrowLeftRightBox.mqh, ArrowUpDownBox.mqh.

Let's start creating new auxiliary hint objects.


Hint auxiliary object and its derivatives

The concept of building such objects will be no different from the concept of building most library objects. There will be a base object here, and its descendants will refine and implement the necessary functionality of a particular hint object.


In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\, create a new file HintBase.mqh of the CHintBase class. The class should be derived from the CWinFormBase base class of all WinForms objects, while its file should be included into the file of the created class:

//+------------------------------------------------------------------+
//|                                                     HintBase.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\WinFormBase.mqh"
//+------------------------------------------------------------------+
//| Class of the base Hint object of the WForms controls             |
//+------------------------------------------------------------------+
class CHintBase : public CWinFormBase
  {
  }


In the protected section, I will write a virtual method for drawing a hint and declare a protected constructor. In the public section of the class, I will write the methods for setting and returning the hint color and declare the parametric constructor and virtual methods for showing, redrawing and clearing the object, as well as the method for drawing the object frame:

//+------------------------------------------------------------------+
//| Class of the base Hint object of the WForms controls             |
//+------------------------------------------------------------------+
class CHintBase : public CWinFormBase
  {
private:

protected:
   //--- Draw a hint
   virtual void      DrawHint(const int shift) {return;}
//--- Protected constructor with object type, chart ID and subwindow
                     CHintBase(const ENUM_GRAPH_ELEMENT_TYPE type,
                               CGCnvElement *main_obj,CGCnvElement *base_obj,
                               const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);
public:
//--- (1) Set and (2) return the hint color
   void              SetHintColor(const color clr)       { this.SetForeColor(clr,false);  }
   color             HintColor(void)               const { return this.ForeColor();       }
//--- Constructor
                     CHintBase(CGCnvElement *main_obj,CGCnvElement *base_obj,
                               const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);
//--- Display the element
   virtual void      Show(void);
//--- Redraw the object
   virtual void      Redraw(bool redraw);
//--- Clear the element filling it with color and opacity
   virtual void      Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Clear the element with a gradient fill
   virtual void      Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false);
//--- Draw the hint frame
   virtual void      DrawFrame(void);
  };
//+------------------------------------------------------------------+

The methods that set and return the hint color actually set and return the object text color - ForeColor().

Let's consider the declared methods in details.

Protected constructor:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintBase::CHintBase(const ENUM_GRAPH_ELEMENT_TYPE type,
                     CGCnvElement *main_obj,CGCnvElement *base_obj,
                     const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CWinFormBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
   this.SetPaddingAll(0);
   this.SetMarginAll(0);
   this.SetBorderSizeAll(0);
  }
//+------------------------------------------------------------------+

The type of the created object, the pointers to the main and base objects, and the rest basic parameters of the created object that are standard for all such constructors are passed to the constructor. In the initialization string, the parent class constructor gets the object type passed to the method, the pointers to the main and base objects and other properties passed in the constructor formal parameters. In the body of the class, set the type of the element passed to the method, the type of the graphical object of the library as an auxiliary object, as well as set the values of Padding, Margin and BorderSize to zero.

Parametric constructor:

//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintBase::CHintBase(CGCnvElement *main_obj,CGCnvElement *base_obj,
                     const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CWinFormBase(GRAPH_ELEMENT_TYPE_WF_HINT_BASE,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_HINT_BASE);
   this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
   this.SetPaddingAll(0);
   this.SetMarginAll(0);
   this.SetBorderSizeAll(0);
  }
//+------------------------------------------------------------------+

Here we pass the pointers to the main and base objects and other standard parameters for creating an object to the constructor just like we do now for all WinForms objects of the library. In the initialization string, pass the type of the object being created as the base hint object to the parent object. Pass the pointers to the main and base objects, as well as other parameters passed to the constructor. In the body of the constructor, specify the type of the created object as the base hint object and the type of the graphical object of the library as an auxiliary object.


The method redrawing an object:

//+------------------------------------------------------------------+
//| Redraw the object                                                |
//+------------------------------------------------------------------+
void CHintBase::Redraw(bool redraw)
  {
//--- Fill the object with background color having transparency
   this.Erase(this.BackgroundColor(),this.Opacity(),true);
  }
//+------------------------------------------------------------------+

Here we simply call the object clearing method with the background color and opacity set for the object.


The method clearing the element filling it with color and opacity:

//+------------------------------------------------------------------+
//| Clear the element filling it with color and opacity              |
//+------------------------------------------------------------------+
void CHintBase::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Fill the element having the specified color and the redrawing flag
   CGCnvElement::EraseNoCrop(colour,opacity,false);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Draw a hint
   this.DrawHint(1);
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

The method logic is fully described in the code comments.


The method that clears an element with a gradient fill:

//+------------------------------------------------------------------+
//| Clear the element with a gradient fill                           |
//+------------------------------------------------------------------+
void CHintBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Fill the element having the specified color array and the redrawing flag
   CGCnvElement::EraseNoCrop(colors,opacity,vgradient,cycle,false);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Draw a hint
   this.DrawHint(1);
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

This method, just like the previous one, fills the background with color from the array of colors, as well as draws a frame and a hint.


The method that draws the border of an element:

//+------------------------------------------------------------------+
//| Draw the element border                                          |
//+------------------------------------------------------------------+
void CHintBase::DrawFrame(void)
  {
   this.DrawRectangle(0,0,this.Width()-1,this.Height()-1,this.BorderColor(),this.Opacity());
  }
//+------------------------------------------------------------------+

Here, we use the DrawRectangle method to draw a frame around the edges of the object with the frame color and opacity set for it.


The method showing the element:

//+------------------------------------------------------------------+
//| Show the element                                                 |
//+------------------------------------------------------------------+
void CHintBase::Show(void)
  {
//--- If the element should not be displayed (hidden inside another control), leave
   if(!this.Displayed())
      return;
//--- If the object has a shadow, display it
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Display the main form
   CGCnvElement::Show();
   this.Redraw(false);
//--- In the loop by all bound graphical objects,
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- get the next graphical element
      CGCnvElement *element=this.m_list_elements.At(i);
      if(element==NULL || !element.Displayed())
         continue;
      //--- and display it
      element.Show();
     }
  }
//+------------------------------------------------------------------+

The method logic is fully described in the code comments.


All other hint objects will be created based on this object. Today I will create objects that hint about the possibility of shifting the separator to the left, right, up and down. They will display appropriate arrows.

Let's create a hint object about the possibility of shifting to the left.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\, create a new file HintMoveLeft.mqh of the CHintMoveLeft class. The class should be derived from the hint object base class, while its file should be included into the created class file:

//+------------------------------------------------------------------+
//|                                                 HintMoveLeft.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "HintBase.mqh"
//+------------------------------------------------------------------+
//| HintMoveLeft base object class of the WForms controls            |
//+------------------------------------------------------------------+
class CHintMoveLeft : public CHintBase
  {
  }


In the protected section of the class, declare a virtual method for drawing a hint that overrides this method of the parent class, as well as declare the protected class constructor. In the public section, declare the 'The cursor is inside the active area, the mouse buttons are not clicked' event handler and the parametric constructor:

//+------------------------------------------------------------------+
//| HintMoveLeft object class of the WForms controls                 |
//+------------------------------------------------------------------+
class CHintMoveLeft : public CHintBase
  {

protected:
   //--- Draw a hint
   virtual void      DrawHint(const int shift);
//--- Protected constructor with object type, chart ID and subwindow
                     CHintMoveLeft(const ENUM_GRAPH_ELEMENT_TYPE type,
                                   CGCnvElement *main_obj,CGCnvElement *base_obj,
                                   const long chart_id,
                                   const int subwindow,
                                   const string descript,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h);
public:
//--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Constructor
                     CHintMoveLeft(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                   const long chart_id,
                                   const int subwindow,
                                   const string descript,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h);
  };
//+------------------------------------------------------------------+


Let's consider the declared methods in details.

Protected constructor:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveLeft::CHintMoveLeft(const ENUM_GRAPH_ELEMENT_TYPE type,
                             CGCnvElement *main_obj,CGCnvElement *base_obj,
                             const long chart_id,
                             const int subwindow,
                             const string descript,
                             const int x,
                             const int y,
                             const int w,
                             const int h) : CHintBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
  }
//+------------------------------------------------------------------+

The type of the created object, the pointers to the main and base objects, and other parameters necessary to create a new graphical element are passed to the constructor. In the initialization string, the type of the created object and all other properties specified in the constructor variables are passed to the parent class constructor. In the class body, indicate the type of the object being created passed in the formal parameters of the constructor.


Parametric constructor:

//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveLeft::CHintMoveLeft(CGCnvElement *main_obj,CGCnvElement *base_obj,
                             const long chart_id,
                             const int subwindow,
                             const string descript,
                             const int x,
                             const int y,
                             const int w,
                             const int h) : CHintBase(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT);
  }
//+------------------------------------------------------------------+

Everything here is exactly the same as in the protected constructor, except thatthe type of the created object is not passed in the formal parameters, but is hardcoded in the code instead.


The method drawing a hint:

//+------------------------------------------------------------------+
//| Draw a hint                                                      |
//+------------------------------------------------------------------+
void CHintMoveLeft::DrawHint(const int shift)
  {
   int w=this.Width();
   int h=this.Height();
   int middle=int(h*0.5);
   this.DrawRectangleFill(w-2,0,w-1,h-1,this.HintColor(),255);
   this.DrawTriangleFill(shift,middle,shift+3,middle-3,shift+3,middle+3,this.HintColor(),255);
   this.DrawLine(shift+3,middle,w-3,middle,this.HintColor(),255);
  }
//+------------------------------------------------------------------+

Here we get the width and height of the entire object and calculate the center line the values for building the arrow will be counted from. Draw a two-pixel-wide filled vertical rectangle on the right side of the object. Draw a triangle whose angles are counted from the 'shift' value, passed to the method, and the center line. Finally, draw a horizontal center line, starting from the left edge indented by the 'shift' value passed to the method, and up to the drawn vertical rectangle on the right side of the object. Thus, we get the left arrow with a base on the right side of the object. In this case, the length of the arrow depends on the 'shift' value passed to the method. The higher the value of this variable, the shorter the arrow.

When hovering the mouse cursor over the separator area in the SplitContainer control, a dotted rectangle appears. Two left-right or up-down arrows should also appear here, depending on how the separator is located - vertically or horizontally. As soon as the mouse cursor moves away from the separator area, the dotted rectangles and arrows should disappear. This happens in the "Cursor in the active area" event handler of the SplitContainer object panels. But if the cursor enters the area of auxiliary objects, then the panel handler will not be called. So, we need to make such a handler in the auxiliary objects as well. All such virtual handlers are declared in the form object class and should be overridden in derived classes. Let's redefine it here.

"The cursor is inside the active area, the mouse buttons are not clicked" event handler:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CHintMoveLeft::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Set the flag for not displaying the object and hide it
   this.SetDisplayed(false);
   this.Hide();
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- From the base object, get the pointers to the "right, up and down shift" hint objects,
//--- set the object non-display flag for them and hide the controls
   CWinFormBase *hint_mr=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,0);
   if(hint_mr!=NULL)
     {
      hint_mr.SetDisplayed(false);
      hint_mr.Hide();
     }
   CWinFormBase *hint_mu=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,0);
   if(hint_mu!=NULL)
     {
      hint_mu.SetDisplayed(false);
      hint_mu.Hide();
     }
   CWinFormBase *hint_md=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,0);
   if(hint_md!=NULL)
     {
      hint_md.SetDisplayed(false);
      hint_md.Hide();
     }
  }
//+------------------------------------------------------------------+

The entire method logic is described in the comments to the code. In short, as soon as the cursor enters the active area of the object, this handler is triggered. We first hide this object in it. Then we get the pointer to the base object hint objects are attached to. In this method, we get hints with right, up and down arrows. The left arrow is this object, and it is already hidden. If the pointers to the requested objects are valid, we hide these objects as well. Thus, when the cursor enters the area of this object, it hides itself and other hint objects.


Let's create an object hinting about the possibility of shifting to the right.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\, create a new file HintMoveRight.mqh of the CHintMoveRight class. The class should be derived from the hint object base class, while its file should be included into the created class file:

//+------------------------------------------------------------------+
//|                                                HintMoveRight.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "HintBase.mqh"
//+------------------------------------------------------------------+
//| Class of the base HintMoveRight object of the WForms controls    |
//+------------------------------------------------------------------+
class CHintMoveRight : public CHintBase
  {
  }

The class is identical to the one above except for the method that draws the hint and the mouse event handler:

//+------------------------------------------------------------------+
//| Class of the HintMoveRight object of the WForms controls         |
//+------------------------------------------------------------------+
class CHintMoveRight : public CHintBase
  {

protected:
   //--- Draw a hint
   virtual void      DrawHint(const int shift);
//--- Protected constructor with object type, chart ID and subwindow
                     CHintMoveRight(const ENUM_GRAPH_ELEMENT_TYPE type,
                                    CGCnvElement *main_obj,CGCnvElement *base_obj,
                                    const long chart_id,
                                    const int subwindow,
                                    const string descript,
                                    const int x,
                                    const int y,
                                    const int w,
                                    const int h);
public:
//--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Constructor
                     CHintMoveRight(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                    const long chart_id,
                                    const int subwindow,
                                    const string descript,
                                    const int x,
                                    const int y,
                                    const int w,
                                    const int h);
  };
//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveRight::CHintMoveRight(const ENUM_GRAPH_ELEMENT_TYPE type,
                               CGCnvElement *main_obj,CGCnvElement *base_obj,
                               const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h) : CHintBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
  }
//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveRight::CHintMoveRight(CGCnvElement *main_obj,CGCnvElement *base_obj,
                              const long chart_id,
                              const int subwindow,
                              const string descript,
                              const int x,
                              const int y,
                              const int w,
                              const int h) : CHintBase(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT);
  }
//+------------------------------------------------------------------+


The method drawing a hint:

//+------------------------------------------------------------------+
//| Draw a hint                                                      |
//+------------------------------------------------------------------+
void CHintMoveRight::DrawHint(const int shift)
  {
   int w=this.Width();
   int h=this.Height();
   int middle=int(h*0.5);
   this.DrawRectangleFill(0,0,1,h-1,this.HintColor(),255);
   this.DrawTriangleFill(shift+8,middle,shift+8-3,middle+3,shift+8-3,middle-3,this.HintColor(),255);
   this.DrawLine(2,middle,shift+4,middle,this.HintColor(),255);
  }
//+------------------------------------------------------------------+

Here we draw a vertical rectangle on the left side of the object, draw an arrow triangle from the 'shift' value plus 8 (line length), as well as draw a horizontal center line starting from the drawn base rectangle and ending with the drawn triangle. When increasing 'shift', the arrow length increases.

"The cursor is inside the active area, the mouse buttons are not clicked" event handler:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CHintMoveRight::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Set the flag for not displaying the object and hide it
   this.SetDisplayed(false);
   this.Hide();
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- From the base object, get the pointers to the "left, up and down shift" hint objects,
//--- set the object non-display flag for them and hide the controls
   CWinFormBase *hint_ml=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,0);
   if(hint_ml!=NULL)
     {
      hint_ml.SetDisplayed(false);
      hint_ml.Hide();
     }
   CWinFormBase *hint_mu=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,0);
   if(hint_mu!=NULL)
     {
      hint_mu.SetDisplayed(false);
      hint_mu.Hide();
     }
   CWinFormBase *hint_md=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,0);
   if(hint_md!=NULL)
     {
      hint_md.SetDisplayed(false);
      hint_md.Hide();
     }
  }
//+------------------------------------------------------------------+

The logic of the method is identical to the logic of this method in the previous class. The only difference is that here this object is a right arrow, so in this method, after hiding this object, we get the pointers to hint objects with left, up and down arrows.


I will consider the other two classes of hint objects with up and down arrows as they are - their logic is identical to the two classes discussed above and it makes no sense to repeat it.

The class of the auxiliary object indicating the possibility of shifting up:

//+------------------------------------------------------------------+
//|                                                   HintMoveUp.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "HintBase.mqh"
//+------------------------------------------------------------------+
//| HintMoveUp object class of WForms controls                       |
//+------------------------------------------------------------------+
class CHintMoveUp : public CHintBase
  {

protected:
   //--- Draw a hint
   virtual void      DrawHint(const int shift);
//--- Protected constructor with object type, chart ID and subwindow
                     CHintMoveUp(const ENUM_GRAPH_ELEMENT_TYPE type,
                                 CGCnvElement *main_obj,CGCnvElement *base_obj,
                                 const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
public:
//--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Constructor
                     CHintMoveUp(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                 const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
  };
//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveUp::CHintMoveUp(const ENUM_GRAPH_ELEMENT_TYPE type,
                         CGCnvElement *main_obj,CGCnvElement *base_obj,
                         const long chart_id,
                         const int subwindow,
                         const string descript,
                         const int x,
                         const int y,
                         const int w,
                         const int h) : CHintBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
  }
//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveUp::CHintMoveUp(CGCnvElement *main_obj,CGCnvElement *base_obj,
                         const long chart_id,
                         const int subwindow,
                         const string descript,
                         const int x,
                         const int y,
                         const int w,
                         const int h) : CHintBase(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP);
  }
//+------------------------------------------------------------------+
//| Draw a hint                                                      |
//+------------------------------------------------------------------+
void CHintMoveUp::DrawHint(const int shift)
  {
   int w=this.Width();
   int h=this.Height();
   int middle=int(w*0.5);
   this.DrawRectangleFill(0,h-2,w-1,h-1,this.HintColor(),255);
   this.DrawTriangleFill(middle,shift,middle+3,shift+3,middle-3,shift+3,this.HintColor(),255);
   this.DrawLine(middle,shift+4,middle,h-3,this.HintColor(),255);
  }
//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CHintMoveUp::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Set the flag for not displaying the object and hide it
   this.SetDisplayed(false);
   this.Hide();
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- From the base object, get the pointers to the "down, left and right shift" hint objects,
//--- set the object non-display flag for them and hide the controls
   CWinFormBase *hint_md=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,0);
   if(hint_md!=NULL)
     {
      hint_md.SetDisplayed(false);
      hint_md.Hide();
     }
   CWinFormBase *hint_ml=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,0);
   if(hint_ml!=NULL)
     {
      hint_ml.SetDisplayed(false);
      hint_ml.Hide();
     }
   CWinFormBase *hint_mr=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,0);
   if(hint_mr!=NULL)
     {
      hint_mr.SetDisplayed(false);
      hint_mr.Hide();
     }
  }
//+------------------------------------------------------------------+


The class of the auxiliary object indicating the possibility of shifting down:

//+------------------------------------------------------------------+
//|                                                 HintMoveDown.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "HintBase.mqh"
//+------------------------------------------------------------------+
//| HintMoveDown object class of WForms controls                     |
//+------------------------------------------------------------------+
class CHintMoveDown : public CHintBase
  {

protected:
   //--- Draw a hint
   virtual void      DrawHint(const int shift);
//--- Protected constructor with object type, chart ID and subwindow
                     CHintMoveDown(const ENUM_GRAPH_ELEMENT_TYPE type,
                                   CGCnvElement *main_obj,CGCnvElement *base_obj,
                                   const long chart_id,
                                   const int subwindow,
                                   const string descript,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h);
public:
//--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Constructor
                     CHintMoveDown(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                   const long chart_id,
                                   const int subwindow,
                                   const string descript,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h);
  };
//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveDown::CHintMoveDown(const ENUM_GRAPH_ELEMENT_TYPE type,
                             CGCnvElement *main_obj,CGCnvElement *base_obj,
                             const long chart_id,
                             const int subwindow,
                             const string descript,
                             const int x,
                             const int y,
                             const int w,
                             const int h) : CHintBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
  }
//+------------------------------------------------------------------+
//| Constructor indicating the main and base objects,                |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CHintMoveDown::CHintMoveDown(CGCnvElement *main_obj,CGCnvElement *base_obj,
                             const long chart_id,
                             const int subwindow,
                             const string descript,
                             const int x,
                             const int y,
                             const int w,
                             const int h) : CHintBase(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN);
  }
//+------------------------------------------------------------------+
//| Draw a hint                                                      |
//+------------------------------------------------------------------+
void CHintMoveDown::DrawHint(const int shift)
  {
   int w=this.Width();
   int h=this.Height();
   int middle=int(w*0.5);
   this.DrawRectangleFill(0,0,w-1,1,this.HintColor(),255);
   this.DrawTriangleFill(middle,shift+8,middle-3,shift+8-3,middle+3,shift+8-3,this.HintColor(),255);
   this.DrawLine(middle,2,middle,shift+8-4,this.HintColor(),255);
  }
//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CHintMoveDown::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Set the flag for not displaying the object and hide it
   this.SetDisplayed(false);
   this.Hide();
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- From the base object, get the pointers to the "up, left and right shift" hint objects,
//--- set the object non-display flag for them and hide the controls
   CWinFormBase *hint_mu=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,0);
   if(hint_mu!=NULL)
     {
      hint_mu.SetDisplayed(false);
      hint_mu.Hide();
     }
   CWinFormBase *hint_ml=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,0);
   if(hint_ml!=NULL)
     {
      hint_ml.SetDisplayed(false);
      hint_ml.Hide();
     }
   CWinFormBase *hint_mr=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,0);
   if(hint_mr!=NULL)
     {
      hint_mr.SetDisplayed(false);
      hint_mr.Hide();
     }
  }
//+------------------------------------------------------------------+

Such objects should usually work in pairs - the left arrow object works together with the right arrow object, the up arrow object works with the down arrow one. It is for this reason that when you specify an offset in the tooltip drawing methods, the arrow decreases for one object and increases for the opposite object. Thus, it is easy to create an animated tooltip with mutual displacement of the arrows of two objects working in pairs. If the arrow of the object on the left decreases, then the arrow of the object on the right increases it. By passing the same shift to their hints drawing methods in a loop, we can achieve a constant shift of the left-right arrows thereby animating the hint.


In all container object classes, we need to add the ability to create these new objects.

In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh file of the base container object class, namely in the method setting the parameters for the added object, remove the strings for setting the main and base objects:

//+------------------------------------------------------------------+
//| Set parameters for the attached object                           |
//+------------------------------------------------------------------+
void CContainer::SetObjParams(CWinFormBase *obj,const color colour)
  {
   obj.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
//--- Set the text color of the object to be the same as that of the base container
   obj.SetForeColor(this.ForeColor(),true);

At the very end, add setting the minimum parameters for the newly created objects:

//---...
//---...
      //--- For the "ArrowButton" WinForms object
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         :
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      :
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    :
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    :
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   :
        obj.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
        obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
        break;
      //--- For "Hint" WinForms object
      case GRAPH_ELEMENT_TYPE_WF_HINT_BASE            :
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetBorderColor(CLR_CANV_NULL,true);
        obj.SetForeColor(CLR_CANV_NULL,true);
        obj.SetOpacity(0,false);
        obj.SetBorderStyle(FRAME_STYLE_NONE);
        break;
      //--- For "HintMoveLeft", "HintMoveRight", "HintMoveUp" and "HintMoveDown" WinForms object 
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT       :
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT      :
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP         :
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN       :
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetBorderColor(CLR_CANV_NULL,true);
        obj.SetForeColor(CLR_DEF_CONTROL_HINT_FORE_COLOR,true);
        obj.SetOpacity(0,false);
        obj.SetBorderStyle(FRAME_STYLE_NONE);
        break;
      default:
        break;
     }
   obj.Crop();
  }
//+------------------------------------------------------------------+


In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh file of the panel object class, namely in the list of included files, add the files of the newly created classes:

//+------------------------------------------------------------------+
//|                                                        Panel.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "..\Helpers\TabField.mqh"
#include "..\Helpers\ArrowUpButton.mqh"
#include "..\Helpers\ArrowDownButton.mqh"
#include "..\Helpers\ArrowLeftButton.mqh"
#include "..\Helpers\ArrowRightButton.mqh"
#include "..\Helpers\ArrowUpDownBox.mqh"
#include "..\Helpers\ArrowLeftRightBox.mqh"
#include "..\Helpers\HintMoveLeft.mqh"
#include "..\Helpers\HintMoveRight.mqh"
#include "..\Helpers\HintMoveUp.mqh"
#include "..\Helpers\HintMoveDown.mqh"
#include "GroupBox.mqh"
#include "TabControl.mqh"
#include "SplitContainer.mqh"
#include "..\..\WForms\Common Controls\ListBox.mqh"
#include "..\..\WForms\Common Controls\CheckedListBox.mqh"
#include "..\..\WForms\Common Controls\ButtonListBox.mqh"
//+------------------------------------------------------------------+

Now these objects will become available for creation in all library container objects.

Keep in mind that the constructors of all classes of WinForms objects have already been modified to specify the main and base objects, and I will not consider these changes further here.

In the method creating a new graphical object, add the above created strings for implementing new library objects:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       const int obj_num,
                                       const string descript,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h,
                                       const color colour,
                                       const uchar opacity,
                                       const bool movable,
                                       const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.GetMain(),this.GetObject(),this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);               break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);            break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);  break;
      case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER      : element=new CSplitContainer(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_SPLITTER             : element=new CSplitter(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_BASE            : element=new CHintBase(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT       : element=new CHintMoveLeft(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT      : element=new CHintMoveRight(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP         : element=new CHintMoveUp(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN       : element=new CHintMoveDown(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Depending on the object type passed to the method, a new object of the corresponding class, whose constructor now receives the pointers to the main and base objects, is created.

In the method that creates the underlay object, specify the main and base objects for the created object:

//+------------------------------------------------------------------+
//| Create the underlay object                                       |
//+------------------------------------------------------------------+
bool CPanel::CreateUnderlayObj(void)
  {
   this.m_underlay=new CGCnvElement(GRAPH_ELEMENT_TYPE_WF_UNDERLAY,this.GetMain(),this.GetObject(),this.ID(),this.Number(),
                                    this.ChartID(),this.SubWindow(),this.NameObj()+"Underlay",
                                    this.CoordXWorkspace(),this.CoordYWorkspace(),this.WidthWorkspace(),this.HeightWorkspace(),
                                    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;
  }
//+------------------------------------------------------------------+


In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh file of the GroupBox control class, namely in the method creating a new graphical object, add the same improvements as in the above class of the panel object:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CGroupBox::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int obj_num,
                                          const string descript,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.GetMain(),this.GetObject(),this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);               break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);            break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);  break;
      case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER      : element=new CSplitContainer(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_SPLITTER             : element=new CSplitter(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_BASE            : element=new CHintBase(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT       : element=new CHintMoveLeft(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT      : element=new CHintMoveRight(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);      break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP         : element=new CHintMoveUp(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN       : element=new CHintMoveDown(this.GetMain(),this.GetObject(),this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Identical improvements to this method are made in all other classes of container objects in the TabControl.mqh, TabField.mqh and SplitContainerPanel.mqh files. Further refinements of these methods will not be considered here.


In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh file of the TabControl class, namely in the CreateTabPages() method creating the specified number of tabs, remove all strings of installing the main and base objects for tab header objects

      header.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      header.SetBase(this.GetObject());

for tab field objects

      field.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      field.SetBase(this.GetObject());

for right-left and up-down button objects

//--- Create the left-right button object
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
//--- Get the pointer to a newly created object
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      box_lr.SetBase(this.GetObject());
      box_lr.SetID(this.GetMaxIDAll());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(0);
      box_lr.Hide();
      CArrowLeftButton *lb=box_lr.GetArrowLeftButton();
      if(lb!=NULL)
        {
         lb.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         lb.SetBase(box_lr);
         lb.SetID(this.GetMaxIDAll());
        }
      CArrowRightButton *rb=box_lr.GetArrowRightButton();
      if(rb!=NULL)
        {
         rb.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         rb.SetBase(box_lr);
         rb.SetID(this.GetMaxIDAll());
        }
     }
//--- Create the up-down button object
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false);
//--- Get the pointer to a newly created object
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      box_ud.SetBase(this.GetObject());
      box_ud.SetID(this.GetMaxIDAll());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(0);
      box_ud.Hide();
      CArrowDownButton *db=box_ud.GetArrowDownButton();
      if(db!=NULL)
        {
         db.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         db.SetBase(box_ud);
         db.SetID(this.GetMaxIDAll());
        }
      CArrowUpButton *ub=box_ud.GetArrowUpButton();
      if(ub!=NULL)
        {
         ub.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         ub.SetBase(box_ud);
         ub.SetID(this.GetMaxIDAll());
        }
     }
//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+


Let's improve the SplitContainer control class in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\SplitContainer.mqh.

In the public section of the class, write the methods returning the pointers to hint objects:

//--- Return the pointer to the separator
   CSplitter        *GetSplitter(void)                         { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_SPLITTER,0);                      }
//--- Return a pointer to the (1) "Left shift", (1) "Right shift", (1) "Up shift" and (1) "Down shift" hint objects
   CHintMoveLeft    *GetHintMoveLeft(void)                     { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,0);                }
   CHintMoveRight   *GetHintMoveRight(void)                    { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,0);               }
   CHintMoveUp      *GetHintMoveUp(void)                       { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,0);                  }
   CHintMoveDown    *GetHintMoveDown(void)                     { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,0);                }

//--- (1) set and (2) return the minimum possible size of the panel 1 and 2


In the method creating panels, remove the loop, in which the main and base objects are set for the created panels, as well as their setting for the separator object:

         return;
      for(int i=0;i<2;i++)
        {
         CSplitContainerPanel *panel=this.GetPanel(i);
         if(panel==NULL)
            continue;
         panel.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         panel.SetBase(this.GetObject());
        }
      //---
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLITTER,this.m_splitter_x,this.m_splitter_y,this.m_splitter_w,this.m_splitter_h,clrNONE,255,true,false))
         return;
      CSplitter *splitter=this.GetSplitter();
      if(splitter!=NULL)
        {
         splitter.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         splitter.SetBase(this.GetObject());
         splitter.SetMovable(true);
         splitter.SetDisplayed(false);
         splitter.Hide();
        }


Besides, add creation of four hint objects and setting their parameters to the method:

//+------------------------------------------------------------------+
//| Create the panels                                                |
//+------------------------------------------------------------------+
void CSplitContainer::CreatePanels(void)
  {
   this.m_list_elements.Clear();
   if(this.SetsPanelParams())
     {
      //--- Create two panels
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel1_x,this.m_panel1_y,this.m_panel1_w,this.m_panel1_h,clrNONE,255,true,false))
         return;
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel2_x,this.m_panel2_y,this.m_panel2_w,this.m_panel2_h,clrNONE,255,true,false))
         return;
      //--- Create a separator object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLITTER,this.m_splitter_x,this.m_splitter_y,this.m_splitter_w,this.m_splitter_h,clrNONE,255,true,false))
         return;
      CSplitter *splitter=this.GetSplitter();
      if(splitter!=NULL)
        {
         splitter.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,this.SplitterOrientation());
         splitter.SetMovable(true);
         splitter.SetDisplayed(false);
         splitter.Hide();
        }
      //--- Create the HintMoveLeft object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,this.m_splitter_x-DEF_HINT_ICON_SIZE,this.m_splitter_y,DEF_HINT_ICON_SIZE,DEF_HINT_ICON_SIZE,clrNONE,255,true,false))
         return;
      CHintMoveLeft *hint_ml=this.GetHintMoveLeft();
      if(hint_ml!=NULL)
        {
         hint_ml.SetMovable(false);
         hint_ml.SetDisplayed(false);
         hint_ml.Hide();
        }
      //--- Create the HintMoveRight object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,this.m_splitter_x+this.m_splitter_w,this.m_splitter_y,DEF_HINT_ICON_SIZE,DEF_HINT_ICON_SIZE,clrNONE,255,true,false))
         return;
      CHintMoveRight *hint_mr=this.GetHintMoveRight();
      if(hint_mr!=NULL)
        {
         hint_mr.SetMovable(false);
         hint_mr.SetDisplayed(false);
         hint_mr.Hide();
        }
      //--- Create the HintMoveUp object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,this.m_splitter_x,this.m_splitter_y-DEF_HINT_ICON_SIZE,DEF_HINT_ICON_SIZE,DEF_HINT_ICON_SIZE,clrNONE,255,true,false))
         return;
      CHintMoveUp *hint_mu=this.GetHintMoveUp();
      if(hint_mu!=NULL)
        {
         hint_mu.SetMovable(false);
         hint_mu.SetDisplayed(false);
         hint_mu.Hide();
        }
      //--- Create the HintMoveDown object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,this.m_splitter_x,this.m_splitter_y+this.m_splitter_h,DEF_HINT_ICON_SIZE,DEF_HINT_ICON_SIZE,clrNONE,255,true,false))
         return;
      CHintMoveDown *hint_md=this.GetHintMoveDown();
      if(hint_md!=NULL)
        {
         hint_md.SetMovable(false);
         hint_md.SetDisplayed(false);
         hint_md.Hide();
        }
     }
  }
//+------------------------------------------------------------------+

If the hint object has been created, disable the mouse movement for it, set the invisibility flag and hide the created element.


After creating the method setting the separator location, add the value passed to the method into the separator object property to be able to find the object orientation (vertical/horizontal) directly from the separator object:

//+------------------------------------------------------------------+
//| set the separator location                                       |
//+------------------------------------------------------------------+
void CSplitContainer::SetSplitterOrientation(const ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION value,const bool only_prop)
  {
   this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,value);
//--- If there are no panels or separator, leave
   CSplitContainerPanel *p1=this.GetPanel1();
   CSplitContainerPanel *p2=this.GetPanel2();
   CSplitter *sp=this.GetSplitter();
   if(p1==NULL || p2==NULL || sp==NULL)
      return;
//--- Set the orientation property for the separator object
   sp.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,value);
//--- If only setting the property, leave
   if(only_prop)
      return;
//--- Set the parameters of the panels and the separator
   this.SetsPanelParams();
//--- If panel 1 is resized successfully
   if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true))
     {
      //--- If panel 2 coordinates are changed to new ones
      if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true))
        {
         //--- if panel 2 has been successfully resized,
         if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true))
           {
            //--- set new relative coordinates of panel 2
            p2.SetCoordXRelative(p2.CoordX()-this.CoordX());
            p2.SetCoordYRelative(p2.CoordY()-this.CoordY());
           }
        }
      //--- If the size of the separator object has been successfully changed, 
      //--- set new values of separator coordinates
      if(sp.Resize(this.m_splitter_w,this.m_splitter_h,false))
         this.SetSplitterDistance(this.SplitterDistance(),true);
     }
  }
//+------------------------------------------------------------------+

Now, when receiving a pointer to a delimiter from outside this class, you can always know its location, which will be useful to us in the future.


In the event handler that handles the separator movement, hide all available visible hint objects:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CSplitContainer::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Adjust subwindow Y shift
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
//--- If the event ID is moving the separator
   if(id==WF_CONTROL_EVENT_MOVING)
     {
      //--- Get the pointer to the separator object
      CSplitter *splitter=this.GetSplitter();
      if(splitter==NULL || this.SplitterFixed())
         return;
      //--- Get the pointers to hint objects
      CHintMoveLeft *hint_ml=this.GetHintMoveLeft();
      CHintMoveRight *hint_mr=this.GetHintMoveRight();
      CHintMoveUp *hint_mu=this.GetHintMoveUp();
      CHintMoveDown *hint_md=this.GetHintMoveDown();
      if(hint_ml==NULL || hint_mr==NULL || hint_mu==NULL || hint_md==NULL)
         return;
      
      //--- Disable the display of hints and hide them
      hint_ml.SetDisplayed(false);
      hint_ml.Hide();
      hint_mr.SetDisplayed(false);
      hint_mr.Hide();
      hint_mu.SetDisplayed(false);
      hint_mu.Hide();
      hint_md.SetDisplayed(false);
      hint_md.Hide();
         
      //--- Declare the variables for separator coordinates
      int x=(int)lparam;
      int y=(int)dparam;
      //--- Depending on the separator direction,
      switch(this.SplitterOrientation())
        {
         //--- vertical position
         case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
           //--- Set the Y coordinate equal to the Y coordinate of the control element
           y=this.CoordY();
           //--- Adjust the X coordinate so that the separator does not go beyond the control element
           //--- taking into account the resulting minimum width of the panels
           if(x<this.CoordX()+this.Panel1MinSize())
              x=this.CoordX()+this.Panel1MinSize();
           if(x>this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth())
              x=this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth();
           break;
         //---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
         //--- horizontal position of the separator
         default:
           //--- Set the X coordinate equal to the X coordinate of the control element
           x=this.CoordX();
           //--- Adjust the Y coordinate so that the separator does not go beyond the control element
           //--- taking into account the resulting minimum height of the panels
           if(y<this.CoordY()+this.Panel1MinSize())
              y=this.CoordY()+this.Panel1MinSize();
           if(y>this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth())
              y=this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth();
           break;
        }
      //--- Draw an empty rectangle
      this.DrawRectangleEmpty();
      //--- If the separator is shifted by the calculated coordinates,
      if(splitter.Move(x,y,true))
        {
         //--- set the separator relative coordinates
         splitter.SetCoordXRelative(splitter.CoordX()-this.CoordX());
         splitter.SetCoordYRelative(splitter.CoordY()-this.CoordY());
         //--- Depending on the direction of the separator, set its new coordinates
         this.SetSplitterDistance(!this.SplitterOrientation() ? splitter.CoordX()-this.CoordX() : splitter.CoordY()-this.CoordY(),false);
        }
     }
  }
//+------------------------------------------------------------------+

The method handles the move event of the separator object. Before adjusting the size and position of the panels, all auxiliary objects are hidden here. Thus, when you hover over the separator area, hints about the possible direction of the separator shift appear. When we capture the separator with the mouse and start moving it, the hints are hidden.

Similarly, we need to hide hints if the cursor is moved out of the separator area.

Add deletion of a dotted rectangle and hiding hint objects in 'The cursor is inside the active area, the mouse buttons are not clicked' event handler:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CSplitContainer::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- If the separator is non-movable, leave
   if(this.SplitterFixed())
      return;
//--- Draw an empty rectangle in the control area
   this.DrawRectangleEmpty();
//--- Get the pointer to the separator
   CSplitter *splitter=this.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- If the separator is not displayed
   if(!splitter.Displayed())
     {
      //--- Enable the display of the separator, show and redraw it
      splitter.SetDisplayed(true);
      splitter.Show();
      splitter.Redraw(true);
     }
//--- Get the pointer to hint objects
   CHintMoveLeft *hint_ml=this.GetHintMoveLeft();
   CHintMoveRight *hint_mr=this.GetHintMoveRight();
   CHintMoveUp *hint_mu=this.GetHintMoveUp();
   CHintMoveDown *hint_md=this.GetHintMoveDown();
   if(hint_ml==NULL || hint_mr==NULL || hint_mu==NULL || hint_md==NULL)
      return;
   hint_ml.SetDisplayed(false);
   hint_ml.Hide();
   hint_mr.SetDisplayed(false);
   hint_mr.Hide();
   hint_mu.SetDisplayed(false);
   hint_mu.Hide();
   hint_md.SetDisplayed(false);
   hint_md.Hide();
  }
//+------------------------------------------------------------------+

As soon as the cursor is removed from the separator area (located in the control area of the object), the cursor enters the active area of the object. The handler of this event first deletes the dotted rectangle that outlines the separator area and then hides any visible hints.


As soon as the cursor enters the control area where the separator is located, hints should be displayed about the possible shift of the separator left-right or up-down, depending on its orientation.

Add the described functionality in the 'The cursor is inside the control area, no mouse buttons are clicked' event handler:

//+------------------------------------------------------------------+
//| The cursor is inside the control area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CSplitContainer::MouseControlAreaNotPressedHandler(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If the separator is non-movable, leave
   if(this.SplitterFixed())
      return;
//--- Draw an empty rectangle in the control area
   this.DrawRectangleEmpty();
//--- Draw a dotted rectangle in the control area
   this.DrawRectangleDotted();
//--- Get the pointer to the separator
   CSplitter *splitter=this.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- If the separator is not displayed
   if(!splitter.Displayed())
     {
      //--- Enable the display of the separator and show it
      splitter.SetDisplayed(true);
      splitter.Erase(true);
      splitter.Show();
     }
//--- Get the pointers to hint objects
   CHintMoveLeft *hint_ml=this.GetHintMoveLeft();
   CHintMoveRight *hint_mr=this.GetHintMoveRight();
   CHintMoveUp *hint_mu=this.GetHintMoveUp();
   CHintMoveDown *hint_md=this.GetHintMoveDown();
   if(hint_ml==NULL || hint_mr==NULL || hint_mu==NULL || hint_md==NULL)
      return;
//--- Get cursor coordinates
   int x=this.m_mouse.CoordX()-this.CoordX();
   int y=this.m_mouse.CoordY()-this.CoordY();
//--- Depending on the separator direction,
   switch(this.SplitterOrientation())
     {
      //--- vertical position
      case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
        //--- Set and adjust the coordinates of the "left shift" hint object
        x=this.CoordX()+this.m_splitter_x-hint_ml.Width();//-1;
        y+=this.CoordY()-hint_ml.Height()-1;
        if(y<this.CoordY()+this.m_splitter_y)
           y=this.CoordY()+this.m_splitter_y;
        //--- Shift the hint object to the calculated coordinates, set its visibility flag and display the object
        if(hint_ml.Move(x,y))
          {
           hint_ml.SetCoordXRelative(x-this.CoordX());
           hint_ml.SetCoordYRelative(y-this.CoordY());
           hint_ml.SetDisplayed(true);
           hint_ml.Show();
          }
        //--- Set and adjust the coordinates of the "right shift" hint object
        x=this.CoordX()+this.m_splitter_x+this.m_splitter_w;//+1;
        //--- Shift the hint object to the calculated coordinates, set its visibility flag and display the object
        if(hint_mr.Move(x,y))
          {
           hint_mr.SetCoordXRelative(x-this.CoordX());
           hint_mr.SetCoordYRelative(y-this.CoordY());
           hint_mr.SetDisplayed(true);
           hint_mr.Show();
          }
        break;
      //---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
      //--- horizontal position of the separator
      default:
        //--- Set and adjust the coordinates of the "up shift" hint object
        y=this.CoordY()+this.m_splitter_y-hint_mu.Height();//-1;
        x+=this.CoordX()-hint_mu.Width()-1;
        if(x<this.CoordX()+this.m_splitter_x)
           x=this.CoordX()+this.m_splitter_x;
        //--- Shift the hint object to the calculated coordinates, set its visibility flag and display the object
        if(hint_mu.Move(x,y))
          {
           hint_mu.SetCoordXRelative(x-this.CoordX());
           hint_mu.SetCoordYRelative(y-this.CoordY());
           hint_mu.SetDisplayed(true);
           hint_mu.Show();
          }
        //--- Set and adjust the coordinates of the "down shift" hint object
        y=this.CoordY()+this.m_splitter_y+this.m_splitter_h;//+1;
        //--- Shift the hint object to the calculated coordinates, set its visibility flag and display the object
        if(hint_md.Move(x,y))
          {
           hint_md.SetCoordXRelative(x-this.CoordX());
           hint_md.SetCoordYRelative(y-this.CoordY());
           hint_md.SetDisplayed(true);
           hint_md.Show();
          }
        break;
     }
  }
//+------------------------------------------------------------------+

The logic of the added code block is commented in the method listing. Get the pointers to hint objects, move them to the cursor so that they are located above it and display hint objects.

In the handler of the last mouse event, hide hint objectsif the last event was outside the form or within the active area:

//+------------------------------------------------------------------+
//| Last mouse event handler                                         |
//+------------------------------------------------------------------+
void CSplitContainer::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled() || !this.Displayed())
      return;
   ENUM_MOUSE_FORM_STATE state=this.GetMouseState();
   switch(state)
     {
      //--- The cursor is outside the form, the mouse buttons are not clicked
      //--- The cursor is outside the form, any mouse button is clicked
      //--- The cursor is outside the form, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED        :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED            :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL              :
      case MOUSE_FORM_STATE_NONE                            :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED  ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED         ||
           this.MouseEventLast()==MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED        ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED     ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL       ||
           this.MouseEventLast()==MOUSE_EVENT_NO_EVENT)
          {
            //--- Draw an empty rectangle in the control area
            this.DrawRectangleEmpty();
            //--- Get the pointer to the separator
            CSplitter *splitter=this.GetSplitter();
            if(splitter==NULL)
              {
               ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
               return;
              }
            splitter.SetDisplayed(false);
            splitter.Hide();
            //--- Get the pointers to hint objects
            CHintMoveLeft *hint_ml=this.GetHintMoveLeft();
            CHintMoveRight *hint_mr=this.GetHintMoveRight();
            CHintMoveUp *hint_mu=this.GetHintMoveUp();
            CHintMoveDown *hint_md=this.GetHintMoveDown();
            if(hint_ml==NULL || hint_mr==NULL || hint_mu==NULL || hint_md==NULL)
               return;
            //--- If the hide object is displayed, disable its display and hide it
            if(hint_ml.Displayed())
              {
               hint_ml.SetDisplayed(false);
               hint_ml.Hide();
              }
            if(hint_mr.Displayed())
              {
               hint_mr.SetDisplayed(false);
               hint_mr.Hide();
              }
            if(hint_mu.Displayed())
              {
               hint_mu.SetDisplayed(false);
               hint_mu.Hide();
              }
            if(hint_md.Displayed())
              {
               hint_md.SetDisplayed(false);
               hint_md.Hide();
              }
            //--- Set the current mouse state as the last one
            this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
          }
        break;
      //--- The cursor is inside the form, the mouse buttons are not clicked
      //--- The cursor is inside the form, any mouse button is clicked
      //--- The cursor is inside the form, the mouse wheel is being scrolled
      //--- The cursor is inside the active area, the mouse buttons are not clicked
      //--- The cursor is inside the active area, any mouse button is clicked
      //--- The cursor is inside the active area, the mouse wheel is being scrolled
      //--- The cursor is inside the active area, left mouse button is released
      //--- The cursor is within the window scrolling area, the mouse buttons are not clicked
      //--- The cursor is within the window scrolling area, any mouse button is clicked
      //--- The cursor is within the window scrolling area, the mouse wheel is being scrolled
      //--- The cursor is within the window resizing area, the mouse buttons are not clicked
      //--- The cursor is within the window resizing area, the mouse button (any) is clicked
      //--- The cursor is within the window resizing area, the mouse wheel is being scrolled
      //--- The cursor is within the window resizing area, the mouse buttons are not clicked
      //--- The cursor is within the window resizing area, the mouse button (any) is clicked
      //--- The cursor is within the window separator area, the mouse wheel is being scrolled
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED         :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED             :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL               :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL        :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED     :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL        :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL        :
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED:
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED    :
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL      :
        break;
      //--- MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+


As soon as the cursor enters the panel of the SplitContainer control, it indicates that it has gone beyond the control area where the separator is located. If, at the same time, the handler of such an event of the CSplitContainer class was not triggered for some reason, then such a situation should be handled in the class of the panel object. In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\SplitContainerPanel.mqh file, namely in the 'The cursor is inside the active area, the mouse buttons are not clicked' event handler, add the code for hiding hint objects:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CSplitContainerPanel::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Get the pointer to the base object
   CSplitContainer *base=this.GetBase();
//--- If the base object is not received, or the separator is non-movable, leave
   if(base==NULL || base.SplitterFixed())
      return;
//--- Draw an empty rectangle in the base object control area
   base.DrawRectangleEmpty();
//--- Get the pointer to the separator object from the base object
   CSplitter *splitter=base.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- If the separator is displayed
   if(splitter.Displayed())
     {
      //--- Disable the display of the separator and hide it
      splitter.SetDisplayed(false);
      splitter.Hide();
     }
//--- Get the pointer to the hint objects from the base object
   CHintMoveLeft *hint_ml=base.GetHintMoveLeft();
   CHintMoveRight *hint_mr=base.GetHintMoveRight();
   CHintMoveUp *hint_mu=base.GetHintMoveUp();
   CHintMoveDown *hint_md=base.GetHintMoveDown();
   if(hint_ml==NULL || hint_mr==NULL || hint_mu==NULL || hint_md==NULL)
      return;
//--- If the hide object is displayed, disable its display and hide it
   if(hint_ml.Displayed())
     {
      hint_ml.SetDisplayed(false);
      hint_ml.Hide();
     }
   if(hint_mr.Displayed())
     {
      hint_mr.SetDisplayed(false);
      hint_mr.Hide();
     }
   if(hint_mu.Displayed())
     {
      hint_mu.SetDisplayed(false);
      hint_mu.Hide();
     }
   if(hint_md.Displayed())
     {
      hint_md.SetDisplayed(false);
      hint_md.Hide();
     }
  }
//+------------------------------------------------------------------+

The logic of the method is identical to the logic in the above handlers.


When the cursor is located on the separator object, we need to move the hint objects after the cursor. The movement of the cursor over the separator can be tracked in the class of the separator object. To do this, we need to add an event handler for the event of the cursor being over the active area of the separator to the class, get the pointers to hint objects and move them after the cursor limiting movement only to the separator area.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\Splitter.mqh, declare the virtual event handler:

//--- Clear the element completely
   virtual void      Erase(const bool redraw=false) { CWinFormBase::Erase(redraw);  }
//--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- 'The cursor is inside the active area, a mouse button is clicked (any)' event handler
   virtual void      MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- 'The cursor is inside the active area, the left mouse button is clicked' event handler
   virtual void      MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
  };
//+------------------------------------------------------------------+


Let's write its implementation outside the class body:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| no mouse buttons are clicked' event handler                      |
//+------------------------------------------------------------------+
void CSplitter::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Declare the pointers to hint objects
   CWinFormBase *hint_ml=NULL;
   CWinFormBase *hint_mr=NULL;
   CWinFormBase *hint_mu=NULL;
   CWinFormBase *hint_md=NULL;
//--- Get cursor coordinates
   int x=this.m_mouse.CoordX();
   int y=this.m_mouse.CoordY();
//--- Depending on the separator direction,
   switch((int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION))
     {
      //--- vertical position
      case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
         //--- From the base object, get the pointer to the "left shift" and "right shift" hint objects
         hint_ml=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,0);
         hint_mr=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,0);
         if(hint_ml==NULL || hint_mr==NULL)
            return;
         //--- Shift hints following the cursor
         hint_ml.Move(hint_ml.CoordX(),(y-hint_ml.Height()<this.CoordY() ? this.CoordY() : y-hint_ml.Height()));
         hint_mr.Move(hint_mr.CoordX(),(y-hint_mr.Height()<this.CoordY() ? this.CoordY() : y-hint_mr.Height()),true);
        break;
      //---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
      //--- horizontal position of the separator
      default:
         //--- From the base object, get the pointer to the "shift up" and "shift down" hint objects
         hint_mu=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,0);
         hint_md=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,0);
         if(hint_mu==NULL || hint_md==NULL)
            return;
         //--- Shift hints following the cursor
         hint_mu.Move((x-hint_mu.Width()<this.CoordX() ? this.CoordX() : x-hint_mu.Width()),hint_mu.CoordY());
         hint_md.Move((x-hint_md.Width()<this.CoordX() ? this.CoordX() : x-hint_md.Width()),hint_md.CoordY(),true);
        break;
     }
  }
//+------------------------------------------------------------------+

The method logic is fully described in the code comments. It should be noted that limiting the movement of hint objects is implemented directly when passing the calculated coordinates to the object shift method.


In the \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh file of the graphical element collection class, set passing the pointers to the main and base objects in all WinForms object creation methods:

//--- Create a graphical element object on canvas on a specified chart and subwindow
   int               CreateElement(const long chart_id,
                                   const int subwindow,
                                   const string descript,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   const color clr,
                                   const uchar opacity,
                                   const bool movable,
                                   const bool activity,
                                   const bool redraw=false)
                       {
                        int id=this.GetMaxID()+1;
                        CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,NULL,NULL,id,0,chart_id,subwindow,descript,x,y,w,h,clr,opacity,movable,activity,redraw);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        if(res==ADD_OBJ_RET_CODE_EXIST)
                           obj.SetID(id);
                        obj.Erase(clr,opacity,redraw);
                        return obj.ID();
                       }

Here NULL is set as the main and base objects, since it is from this class that we create independent objects (rather than the ones tied to any other). Therefore, the object will initially be main and independent.

We have many such methods in the class, and all the changes are identical. Therefore, it is not necessary to describe them all here. Let's consider only one of several methods for creating a form object, since passing the pointers in it and subsequent WinForms objects is slightly different from the one in the above method:

//--- Create a graphical form object on canvas on a specified chart and subwindow
   int               CreateForm(const long chart_id,
                                const int subwindow,
                                const string descript,
                                const int x,
                                const int y,
                                const int w,
                                const int h,
                                const color clr,
                                const uchar opacity,
                                const bool movable,
                                const bool activity,
                                const bool shadow=false,
                                const bool redraw=false)
                       {
                        int id=this.GetMaxID()+1;
                        CForm *obj=new CForm(NULL,NULL,chart_id,subwindow,descript,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.SetBackgroundColor(clr,true);
                        obj.SetBorderColor(clr,true);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.BorderColor(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,redraw);
                        return obj.ID();
                       }

Here, we do not pass the type of the object before specifying the main and base objects. It is already registered in the constructor of the created class.

All other changes are identical and have already been made in the class file. Let's test the results.


Test

To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part124\ as TestDoEasy124.mq5.

There will be no changes in the EA. Let's compile and launch it on the chart:


The implemented functionality works correctly.


What's next?

In the next article, I will continue working on hint objects.

All files of the current library version, test EA and chart event control indicator for MQL5 are attached below.

Back to contents

*Previous articles within the series:

 
DoEasy. Controls (Part 20): SplitContainer WinForms object
DoEasy. Controls (Part 21): SplitContainer control. Panel separator
DoEasy. Controls (Part 22): SplitContainer. Changing the properties of the created object
DoEasy. Controls (Part 23): Improving TabControl and SplitContainer WinForms objects