Graphical Interfaces I: Animating the Graphical Interface (Chapter 3)

Anatoli Kazharski | 15 February, 2016

Content

 

Introduction

This article is the continuation of the first part of the series about graphical interfaces. The first article Graphical Interfaces I: Preparation of the Library Structure (Chapter 1) explains in detail what this library is for. A complete list of links to the articles of the first part is at the end of each chapter. There, you can also download a complete version of the library at the current stage of development. The files must be placed in the same directories as they are located in the archive.

In the previous article of the series, we started to develop a form class for controls. In this article, we are going to continue developing this class and fill it with methods for moving the form over the chart area. We will also integrate this interface component into the core of the library. Also, we will ensure that the color of a form control changes when the cursor is hovering over it.

 

Managing the Graphical Interface

A form for controls is to be attached to the chart. Right now it is completely unresponsive. Our goal is to make the form and its controls react to the user's actions. To implement this, it is required to track the cursor position on the chart. The program must "be aware" of the cursor coordinates at any point in time. This is not possible to implement in the MQL application with the default chart parameters. Tracking the cursor position and pressing the mouse buttons have to be enabled. Looking ahead, in the process of development, some chart properties will have to be used. Therefore, let us include the CChart class from the standard library in the WndEvents.mqh file of our library and create its instance in the class body.

//+------------------------------------------------------------------+
//|                                                    WndEvents.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Defines.mqh"
#include "WndContainer.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| Class for event handling                                         |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
   CChart            m_chart;
  };

In the class constructor, attach the object to the current chart, having obtained its identifier, and enable tracking of the cursor position. In the destructor, detach the object from the chart (highlighted in green in the code below). Otherwise, when the program is deleted from the chart, the chart itself will be closing.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void) : m_chart_id(0),
                               m_subwin(0),
                               m_indicator_name(""),
                               m_program_name(PROGRAM_NAME)
  {
//--- Get the ID of the current chart
   m_chart.Attach();
//--- Enable tracking of mouse events
   m_chart.EventMouseMove(true);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
//--- Detach from the chart
   m_chart.Detach();
  }

As mentioned earlier, the class of each control will have its own event handler. Now, in the CWindow class let us create a few methods that we will need for managing the form. All of them will be called in the CWindow::OnEvent() method. Let us decide on the functionality, which will be added to the CWindow class:

1. The window on which the cursor is located must be defined. This is because a chart can consist of several parts, that is the main chart and indicator sub-windows, and the MQL application can be an indicator, located in a window other than the main one.

2. If the MQL application is an indicator and is not located in the main window of the chart, the Y coordinate must be adjusted.

3. The state of the left mouse button has to be checked as well as the point where it was pressed. There will be four states:

  • NOT_PRESSED — the button was not pressed.
  • PRESSED_OUTSIDE — the button was pressed outside of the form area.
  • PRESSED_INSIDE_WINDOW — the button was pressed inside the form area.
  • PRESSED_INSIDE_HEADER — the button pressed inside the header area.

Add the ENUM_WMOUSE_STATE enumeration to the Enums.mqh file:

//+------------------------------------------------------------------+
//|                                                        Enums.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Enumeration of the states of the left mouse button for the form  |
//+------------------------------------------------------------------+
enum ENUM_WMOUSE_STATE
  {
   NOT_PRESSED           =0,
   PRESSED_OUTSIDE       =1,
   PRESSED_INSIDE_WINDOW =2,
   PRESSED_INSIDE_HEADER =3
  };

4. If the cursor is inside the form area or inside the area of an interface component, then the chart scrolling and trading level management must be disabled.

5. If the cursor is inside the capture area of the header and the left mouse button is pressed, then the program enters the mode of updating the form coordinates.

6. When the coordinates are updated, a check for leaving the chart area is carried out and corresponding adjustment is made. Such a check requires (1) object of the CChart class, (2) variables and the method for obtaining the chart sizes.

7. A method is also required for moving all form objects in relation to the updated coordinates. For that, the CWindow::Moving() virtual method has been declared earlier. Now, we have to implement it.

8. We also require some auxiliary methods for identifying the cursor in the header area and for zeroing some auxiliary variables.

9. Tracking the mouse focus will work for all objects constituting the window. Therefore, let us create a method where such a check will be carried out.

10. A graphical interface with the ability to move it over the chart is not always necessary. For this reason, we will add a method that will allow to enable/disable this feature.

 

Functionality for Moving the Form

Let us implement the functionality discussed above. In the class body, add declarations of required methods and some variables. Include file with the CChart class and create its instance (highlighted in yellow):

//+------------------------------------------------------------------+
//|                                                       Window.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| Class for creating a form for controls                           |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
private:
   CChart            m_chart;
   //--- Possibility to move a window on the chart
   bool              m_movable;
   //--- Chart size
   int               m_chart_width;
   int               m_chart_height;
   //--- Variables, connected with the displacement
   int               m_prev_x;        // Fixed point X when clicking
   int               m_prev_y;        // Fixed point Y when clicking
   int               m_size_fixing_x; // Distance from the X coordinate to the fixed point X
   int               m_size_fixing_y; // Distance from the Y coordinate to the fixed point Y
   //--- State of the mouse button considering the position where it was clicked
   ENUM_WMOUSE_STATE m_clamping_area_mouse;
   //---
public:
   //--- Possibility of moving the window
   bool              Movable(void)                                     const { return(m_movable);                  }
   void              Movable(const bool flag)                                { m_movable=flag;                     }
   //--- Getting chart size
   void              SetWindowProperties(void);
   //--- Converts the Y coordinate into a relative one
   void              YToRelative(const int y);
   //--- Checking the cursor in the header area 
   bool              CursorInsideCaption(const int x,const int y);
   //--- Zeroing variables
   void              ZeroPanelVariables(void);
   //--- Verifying the mouse focus
   void              CheckMouseFocus(const int x,const int y,const int subwin);
   //--- Verifying the state of the left mouse button
   void              CheckMouseButtonState(const int x,const int y,const string state);
   //--- Setting the chart mode
   void              SetChartState(const int subwindow_number);
   //--- Updating the form coordinates
   void              UpdateWindowXY(const int x,const int y);
  };

The code of the SetWindowProperties(), YToRelative(), CursorInsideCaption() and ZeroPanelVariables() methods is rather simple and does not require any additional explanations. The only thing I would like to draw your attention to is passing the sub-window number (m_subwin) to the methods of the CChart object. This must be the number of the sub-window that contains the MQL program.

//+------------------------------------------------------------------+
//| Getting chart size                                               |
//+------------------------------------------------------------------+
void CWindow::SetWindowProperties(void)
  {
//--- Get width and height of the chart window
   m_chart_width  =m_chart.WidthInPixels();
   m_chart_height =m_chart.HeightInPixels(m_subwin);
  }
//+------------------------------------------------------------------+
//| Convert the Y coordinate into a relative one                     |
//+------------------------------------------------------------------+
int CWindow::YToRelative(const int y)
  {
//--- Get the distance from the top of the chart to the indicator sub-window
   int chart_y_distance=m_chart.SubwindowY(m_subwin);
//--- Convert the Y coordinate into a relative one
   return(y-chart_y_distance);
  }
//+------------------------------------------------------------------+
//| Verifying the cursor location in the area of the window title    |
//+------------------------------------------------------------------+
bool CWindow::CursorInsideCaption(const int x,const int y)
  {
   return(x>m_x && x<X2()-m_right_limit && y>m_y && y<m_caption_bg.Y2());
  }
//+------------------------------------------------------------------+
//| Zeroing variables, connected with displacement of the window and |
//| the state of the left mouse button                               |
//+------------------------------------------------------------------+
void CWindow::ZeroPanelVariables(void)
  {
   m_prev_x              =0;
   m_prev_y              =0;
   m_size_fixing_x       =0;
   m_size_fixing_y       =0;
   m_clamping_area_mouse =NOT_PRESSED;
  }

The verification of the state of the left mouse button in relation to the form takes place in the CWindow::CheckMouseButtonState() method. For that, the cursor coordinates and the string parameter of the event model of the chart are passed to this method. The string parameter shows the state of the left mouse button when the CHARTEVENT_MOUSE_MOVE event is handled. This means that this parameter can have value of "0", if the mouse button is released and "1", if the mouse button is pressed.

If the button is released, then all auxiliary variables are zeroed and the work of the method is finished. If the button is pressed, then a check is carried out in the CWindow::CheckMouseButtonState() method to determine where on the chart area this occurred. If it turns out that the button already has a recorded state, that is it was pressed in some chart area, then the program will leave the method (return).

//+------------------------------------------------------------------+
//| Verifies the state of the mouse button                           |
//+------------------------------------------------------------------+
void CWindow::CheckMouseButtonState(const int x,const int y,const string state)
  {
//--- If the button is released
   if(state=="0")
     {
      //--- Zero variables
      ZeroPanelVariables();
      return;
     }
//--- If the button is pressed
   if(state=="1")
     {
      //--- Leave, if the state is recorded
      if(m_clamping_area_mouse!=NOT_PRESSED)
         return;
      //--- Outside of the panel area
      if(!CElement::MouseFocus())
         m_clamping_area_mouse=PRESSED_OUTSIDE;
      //--- Inside the panel area
      else
        {
         //--- If inside the header
         if(CursorInsideCaption(x,y))
           {
            m_clamping_area_mouse=PRESSED_INSIDE_HEADER;
            return;
           }
         //--- If inside the window area
         m_clamping_area_mouse=PRESSED_INSIDE_WINDOW;
        }
     }
  }

Methods for defining the object boundaries are provided in the Objects.mqh file of all classes of primitive objects. These methods allow quick and easy identification if an object is in the focus of the mouse. The focus for the form and all form objects can be checked in the CWindow class using the CheckMouseFocus() method. Current coordinates of the cursor and the sub-window number where the cursor is located will be passed to this method. A way of obtaining the sub-window number containing the cursor will be demonstrated later.

//+------------------------------------------------------------------+
//| Verifying the mouse focus                                        |
//+------------------------------------------------------------------+
void CWindow::CheckMouseFocus(const int x,const int y,const int subwin)
  {
//--- If the cursor is in the area of the program window
   if(subwin==m_subwin)
     {
      //--- If currently not in the form displacement mode
      if(m_clamping_area_mouse!=PRESSED_INSIDE_HEADER)
        {
         //--- Verify the cursor location
         CElement::MouseFocus(x>m_x && x<X2() && y>m_y && y<Y2());
         //---
         m_button_rollup.MouseFocus(x>m_button_rollup.X() && x<m_button_rollup.X2() && 
                                    y>m_button_rollup.Y() && y<m_button_rollup.Y2());
         m_button_close.MouseFocus(x>m_button_close.X() && x<m_button_close.X2() && 
                                   y>m_button_close.Y() && y<m_button_close.Y2());
         m_button_unroll.MouseFocus(x>m_button_unroll.X() && x<m_button_unroll.X2() && 
                                    y>m_button_unroll.Y() && y<m_button_unroll.Y2());
        }
     }
   else
     {
      CElement::MouseFocus(false);
     }
  }

The result of the check of the form focus and the state of the mouse button in relation to the form can indicate if the chart scrolling and managing trading levels have to be enabled or disabled. If these properties of the chart are not disabled when the cursor is over the form, then the scrolling of the chart will be happening together with the form displacement and management of the trading levels will happen from under the form, which is not needed.

//+------------------------------------------------------------------+
//| Set the chart state                                              |
//+------------------------------------------------------------------+
void CWindow::SetChartState(const int subwindow_number)
  {
//--- If (the cursor is in the panel area and the mouse button is released) or
//    the mouse button was pressed inside the area of the form or header
   if((CElement::MouseFocus() && m_clamping_area_mouse==NOT_PRESSED) || 
      m_clamping_area_mouse==PRESSED_INSIDE_WINDOW ||
      m_clamping_area_mouse==PRESSED_INSIDE_HEADER)
     {
      //--- Disable scroll and management of trading levels
      m_chart.MouseScroll(false);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,false);
     }
//--- Enable management, if the cursor is outside of the window area
   else
     {
      m_chart.MouseScroll(true);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,true);
     }
  }

Updating of the window coordinates takes place in the CWindow::UpdateWindowXY() method. At the beginning, the mode of the window is checked. If the form is in fixed mode, then the program leaves the method as there is no point to update coordinates. Then, if the mouse button is pressed, the current coordinates get stored, the distance from the edge point of the form to the cursor get stored, limits for exiting the chart area are calculated, checks take place and, if required, adjustment of coordinates for the form. You can study the code below:

//+------------------------------------------------------------------+
//| Updating window coordinates                                      |
//+------------------------------------------------------------------+
void CWindow::UpdateWindowXY(const int x,const int y)
  {
//--- If the mode of fixed form is set
   if(!m_movable)
      return;
//---
   int new_x_point =0; // New X coordinate
   int new_y_point =0; // New Y coordinate
//--- Limits
   int limit_top    =0;
   int limit_left   =0;
   int limit_bottom =0;
   int limit_right  =0;
//--- If the mouse button is pressed
   if((bool)m_clamping_area_mouse)
     {
      //--- Store current XY coordinates of the cursor
      if(m_prev_y==0 || m_prev_x==0)
        {
         m_prev_y=y;
         m_prev_x=x;
        }
      //--- Store the distance from the edge point of the form to the cursor
      if(m_size_fixing_y==0 || m_size_fixing_x==0)
        {
         m_size_fixing_y=m_y-m_prev_y;
         m_size_fixing_x=m_x-m_prev_x;
        }
     }
//--- Set limits
   limit_top    =y-::fabs(m_size_fixing_y);
   limit_left   =x-::fabs(m_size_fixing_x);
   limit_bottom =m_y+m_caption_height;
   limit_right  =m_x+m_x_size;
//--- If the boundaries of the chart are not exceeded downwards/upwards/right/left
   if(limit_bottom<m_chart_height && limit_top>=0 && 
      limit_right<m_chart_width && limit_left>=0)
     {
      new_y_point =y+m_size_fixing_y;
      new_x_point =x+m_size_fixing_x;
     }
//--- If the boundaries of the chart were exceeded
   else
     {
      if(limit_bottom>m_chart_height) // > downwards
        {
         new_y_point =m_chart_height-m_caption_height;
         new_x_point =x+m_size_fixing_x;
        }
      if(limit_top<0) // > upwards
        {
         new_y_point =0;
         new_x_point =x+m_size_fixing_x;
        }
      if(limit_right>m_chart_width) // > right
        {
         new_x_point =m_chart_width-m_x_size;
         new_y_point =y+m_size_fixing_y;
        }
      if(limit_left<0) // > left
        {
         new_x_point =0;
         new_y_point =y+m_size_fixing_y;
        }
     }
//--- Update coordinates, if there was a displacement
   if(new_x_point>0 || new_y_point>0)
     {
      //--- Adjust the form coordinates
      m_x =(new_x_point<=0)? 1 : new_x_point;
      m_y =(new_y_point<=0)? 1 : new_y_point;
      //---
      if(new_x_point>0)
         m_x=(m_x>m_chart_width-m_x_size-1) ? m_chart_width-m_x_size-1 : m_x;
      if(new_y_point>0)
         m_y=(m_y>m_chart_height-m_caption_height-1) ? m_chart_height-m_caption_height-2 : m_y;
      //--- Zero the fixed points
      m_prev_x=0;
      m_prev_y=0;
     }
  }

In the OnEvent() chart event handler of the CWindow class, when handling the event of the mouse movement (CHARTEVENT_MOUSE_MOVE), the sub-window number can be obtained with the ChartXYToTimePrice() function. If the function returns true, then receive a relative Y coordinate using previously created CWindow::YToRelative() method and after that iterate over all the methods listed above:

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      int      x      =(int)lparam; // Coordinate of the X axis
      int      y      =(int)dparam; // Coordinate of the Y axis
      int      subwin =WRONG_VALUE; // Window number, in which the cursor is located
      datetime time   =NULL;        // Time corresponding to the X coordinate
      double   level  =0.0;         // Level (price) corresponding to the Y coordinate
      int      rel_y  =0;           // For identification of the relative Y coordinate
      //--- Get the cursor location
      if(!::ChartXYToTimePrice(m_chart_id,x,y,subwin,time,level))
         return;
      //--- Get the relative Y coordinate
      rel_y=YToRelative(y);
      //--- Verify and store the state of the mouse button
      CheckMouseButtonState(x,rel_y,sparam);
      //--- Verifying the mouse focus
      CheckMouseFocus(x,rel_y,subwin);
      //--- Set the chart state
      SetChartState(subwin);
      //--- If the management is delegated to the window, identify its location
      if(m_clamping_area_mouse==PRESSED_INSIDE_HEADER)
        {
         //--- Updating window coordinates
         UpdateWindowXY(x,rel_y);
        }
      return;
     }
  }

To test the form movement, only the CWindow::Moving() method is left to implement. It may look like this method will be used in the internal event handler of the class straight after the UpdateWindowXY() method but this is not the case. If we do it now, when there are no other controls in the form, the result will be excellent. Following this logic, you will face the necessity to do the same thing in classes of other controls. As a result, when the form has many controls attached and is being moved, all controls will be moved together with the form with some delay. And this will be a bad result.

The reason for that will be that if every method for moving an interface control is implemented in the internal handler, then all controls will be divided by various conditions and checks. That will cause a delay. For all the controls to move synchronously, there must be no other operations between the Moving() methods. That can be implemented in the CWndEvents class as it is derived class from the CWndContainer class and has a direct access to all object pointers, which will be stored there.

The content of the CWindow::Moving() method consists of two parts. At first, coordinates of all objects that constitute the form are stored and then object coordinates get updated on the chart. Similarly, the Moving() method will be implemented for each control later.

//+------------------------------------------------------------------+
//| Moving the window                                                |
//+------------------------------------------------------------------+
void CWindow::Moving(const int x,const int y)
  {
//--- Storing coordinates in variables
   m_bg.X(x);
   m_bg.Y(y);
   m_caption_bg.X(x);
   m_caption_bg.Y(y);
   m_icon.X(x+m_icon.XGap());
   m_icon.Y(y+m_icon.YGap());
   m_label.X(x+m_label.XGap());
   m_label.Y(y+m_label.YGap());
   m_button_close.X(x+m_button_close.XGap());
   m_button_close.Y(y+m_button_close.YGap());
   m_button_unroll.X(x+m_button_unroll.XGap());
   m_button_unroll.Y(y+m_button_unroll.YGap());
   m_button_rollup.X(x+m_button_rollup.XGap());
   m_button_rollup.Y(y+m_button_rollup.YGap());
   m_button_tooltip.X(x+m_button_tooltip.XGap());
   m_button_tooltip.Y(y+m_button_tooltip.YGap());
//--- Updating coordinates of graphical objects
   m_bg.X_Distance(m_bg.X());
   m_bg.Y_Distance(m_bg.Y());
   m_caption_bg.X_Distance(m_caption_bg.X());
   m_caption_bg.Y_Distance(m_caption_bg.Y());
   m_icon.X_Distance(m_icon.X());
   m_icon.Y_Distance(m_icon.Y());
   m_label.X_Distance(m_label.X());
   m_label.Y_Distance(m_label.Y());
   m_button_close.X_Distance(m_button_close.X());
   m_button_close.Y_Distance(m_button_close.Y());
   m_button_unroll.X_Distance(m_button_unroll.X());
   m_button_unroll.Y_Distance(m_button_unroll.Y());
   m_button_rollup.X_Distance(m_button_rollup.X());
   m_button_rollup.Y_Distance(m_button_rollup.Y());
   m_button_tooltip.X_Distance(m_button_tooltip.X());
   m_button_tooltip.Y_Distance(m_button_tooltip.Y());
  }

 

Testing Movement of the Form over the Chart

All events are to be handled in the CWndEvents::ChartEvent() method, which has been created earlier and is currently empty. At the beginning, a check of the size of the window pointers array will be carried out. If it is empty, then there is no point in carrying on with work and the program will leave it (return). Then the class fields that refer to the parameters of chart events will be initialized. We will place two functions of event handling for now: (1) for checking of events in handlers of each control CWndEvents::CheckElementsEvents() and (2) for tracking the mouse cursor CWndEvents::ChartEventMouseMove().

Thus, currently the CWndEvents::ChartEvent() method should have the content as shown in the code below:

//+------------------------------------------------------------------+
//| Program event handling                                           |
//+------------------------------------------------------------------+
void CWndEvents::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If the array is empty, leave
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- Initialization of event parameter fields
   InitChartEventsParams(id,lparam,dparam,sparam);
//--- Verification of interface control events
   CheckElementsEvents();
//--- Event of mouse movement
   ChartEventMouseMove();
  }

Implement the CWndEvents::CheckElementsEvents() and CWndEvents::ChartEventMouseMove() methods as they are currently empty. Checking events of the interface controls is carried out in one cycle. There we call sequentially the OnEvent() handlers of all controls that are in the base. As currently our test file contains only one window, we will temporarily use the zero index of the window arrays in the CWndEvents class. This will have to be changed when we move on to developing the multi-window mode.

//+------------------------------------------------------------------+
//| Verification of the control events                               |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEvents(void)
  {
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
  }

Let us create a method in the CWndEvents class where control movement over the chart will be carried out. We will name it MovingWindow(). At first, in this method the form movement will be executed and then movement of all controls that are attached to it.

class CWndEvents : public CWndContainer
  {
private:
   //--- Moving the window
   void              MovingWindow(void);
  };
//+------------------------------------------------------------------+
//| Moving the window                                                |
//+------------------------------------------------------------------+
void CWndEvents::MovingWindow(void)
  {
//--- Moving the window
   int x=m_windows[0].X();
   int y=m_windows[0].Y();
   m_windows[0].Moving(x,y);
//--- Moving controls
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].Moving(x,y);
  }

Now, the code can be added to the CWndEvents::ChartEventMouseMove() method as shown below. Do not forget to redraw the chart every time after the object coordinates are updated on the chart, otherwise changes will not be reflected.

//+------------------------------------------------------------------+
//| CHARTEVENT MOUSE MOVE event                                      |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventMouseMove(void)
  {
//--- Leave, if this is not an event of the cursor displacement
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      return;
//--- Moving the window
   MovingWindow();
//--- Redraw chart
   m_chart.Redraw();
  }

The process of the library development is in its final stage where the window movement on the chart can be tested. In the body of the CWindow::OnEvent() method, append the code for displaying data in the left top corner of the chart, where in the real time the following will be reflected:

This code is to be deleted after the test.

::Comment("x: ",x,"\n",
                "y: ",y,"\n",
                "rel_y: ",rel_y,"\n",
                "w.x: ",m_x,"\n",
                "w.y: ",m_y,"\n",
                "subwin: ",subwin,"\n",
                "m_subwin: ",m_subwin,"\n",
                "clamping mode: ",m_clamping_area_mouse);

Compile the project files and load the program on to the chart. To verify that the program is tracking correctly in which chart window the cursor is located, load any indicator that renders not on the main chart window.

I am nearly certain that you will not be able to move the window after the program is loaded on to the chart. The reason being that in the constructor of the CWindow class the m_movable variable is initialized by the false value, which means that the user cannot move the form on the chart. That is why the developer of the MQL application must specify in the code the necessity of the form movement.

In the CProgram class of the CreateWindow() method, add a line as shown in the code below, compile files and try to test the application again.

//+------------------------------------------------------------------+
//| Creates a form for controls                                      |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow(const string caption_text)
  {
//--- Add a window pointer to the window array
   CWndContainer::AddWindow(m_window);
//--- Coordinates
   int x=1;
   int y=1;
//--- Properties
   m_window.Movable(true);
   m_window.XSize(200);
   m_window.YSize(200);
//--- Creating a form
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   return(true);
  }

Now, there should not be anything that would stop moving the form:

Fig. 1. Test of moving the form on the chart.

Fig. 1. Testing Movement of the Form over the Chart

Sometimes it happens that the size of the chart window has to be changed. At such moments, the event of changing the chart properties CHARTEVENT_CHART_CHANGE is generated. At the moment, this is not tracked in any way and a situation can occur when the form partially or completely exceeds the boundaries of the chart window. To avoid that, this type of event must be also checked in the CWindow::OnEvent() chart event handler.

Since this event is generated also during the scrolling of a chart, to avoid performing a lot of unnecessary actions, a check is required if the left mouse button was clicked at the moment when the event happened. If the left mouse button is released, then we get the chart size and if the limits of the chart window were exceeded, then we adjust coordinates. Below is the code that has to be added in the end of the CWindow::OnEvent() method after handling other events:

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Event of changing the chart properties
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- If the button is released
      if(m_clamping_area_mouse==NOT_PRESSED)
        {
         //--- Get the chart window size
         SetWindowProperties();
         //--- Adjustment of coordinates
         UpdateWindowXY(m_x,m_y);
        }
      return;
     }
  }

The tangible result of the work described above is the possibility of moving the form for controls. Then, we need to embed functionality into buttons for minimizing and maximizing windows. Besides, these buttons are also meant to react to the mouse cursor movement so the end user understands that they are not only icons or design elements.

 

Changing the Appearance of the Interface Component when the Cursor is Hovering over It

Earlier we considered the implementation of the CElement class, which is base class for all controls. We created the CElement::ChangeObjectColor() method for changing the object color when hovering the cursor over as one of its members. This is the time to create a mechanism which will allow us to use it in our work. Adding such functionality requires a timer. This is disabled in the settings of the MQL application by default. It is up to the application developer to decide whether the timer is to be enabled depending on the set goals.

To enable the timer, the MQL language has two functions with different frequency: EventSetTimer() and EventSetMillisecondTimer(). The first one allows to set an interval no shorter than one second. This does not suit our purposes as one second is too high an interval for a control to change its appearance when the cursor is hovered over it. The change should happen instantly with no delays. We are going to use the EventSetMillisecondTimer() function, which supports setting a timer with intervals measured in milliseconds. According to the MQL reference, a minimal interval that can be set using this function is 10-16 milliseconds. This is enough to realize our plan.

Add a constant with the timer step required for the library to work in the Defines.mqh file:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Timer step (milliseconds)
#define TIMER_STEP_MSC (16)

You may ask logical questions like "It this not too frequent?" and "Is the program going to consume too much resources?". We will answer these questions when we test this idea.

Enable the timer in the CWndEvents class, which is the core of the library under development. For that, add these lines of code in its constructor and destructor as shown below:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void)
  {
//--- Enable the timer
   if(!::MQLInfoInteger(MQL_TESTER))
      ::EventSetMillisecondTimer(TIMER_STEP_MSC);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
//--- Delete the timer
   ::EventKillTimer();
  }

In the constructor of the CWndEvents class, the timer will be enabled only if the program is outside of the strategy tester. Our library will not work in the tester right now as currently it has some limitations for working with graphical objects. Besides, the minimal interval for the timer in the tester is equal to one second, even if you use the EventSetMillisecondTimer() function. In destructor the timer is disabled with the EventKillTimer() function.

In the class of every control there will be its own implementation of the OnEventTimer() method, and for that in the CElement class there is a virtual method with that name. Similar to the CWndEvents::CheckElementsEvents() method, where we iterate over the OnEvent() methods of each control, we have to create a method where the program will also iterate over the OnEventTimer() methods. Let us name it CheckElementsEventsTimer().

Declaration and implementation of the CWndEvents::CheckElementsEventsTimer() method:

class CWndEvents : public CWndContainer
  {
private:
   //--- Checking events of all controls by timer
   void              CheckElementsEventsTimer(void);
  };
//+------------------------------------------------------------------+
//| Checking events of all controls by timer                         |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEventsTimer(void)
  {
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].OnEventTimer();
  }

Then, this method has to be called in the CWndEvents::OnTimerEvent() method with the check for the size of the window array in the very beginning as it was done in the CWndEvents::ChartEvent() method.

//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- If the array is empty, leave  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- Checking events of all controls by timer
   CheckElementsEventsTimer();
//--- Redraw chart
   m_chart.Redraw();
  }

In the CProgram class, where the developer of the MQL application will be creating the graphical interface, in the CProgram::OnTimerEvent() method, which is connected with the main program file, just call the eponymous method from the base class as shown in the code below:

//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();
  }

This way, the actions of all controls that are in the OnEventTimer() methods will be available in the stream of events of the program timer.

We do not have everything ready to test this yet. The library that we are developing currently contains only one interface component — a form for controls, the CWindow class. We are going to create required functionality in it, which will be added to its local version of the OnEventTimer() method.

The name of the color change will be named CWindow::ChangeObjectsColor(). Below is its declaration and implementation in the CWindow class:

class CWindow: public CElement
  {
private:
   //--- Changing the color of the form objects
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Changing of the object color when hovering the cursor over it    |
//+------------------------------------------------------------------+
void CWindow::ChangeObjectsColor(void)
  {
//--- Changing icons in the buttons
   m_button_rollup.State(m_button_rollup.MouseFocus());
   m_button_unroll.State(m_button_unroll.MouseFocus());
   m_button_close.State(m_button_close.MouseFocus());
//--- Changing the color in the header
   CElement::ChangeObjectColor(m_caption_bg.Name(),CElement::MouseFocus(),OBJPROP_BGCOLOR,
                               m_caption_bg_color,m_caption_bg_color_hover,m_caption_color_bg_array);
  }

As you can see from the code, there is nothing complicated about the CWindow::ChangeObjectsColor() method. The MouseFocus() methods return the focus of the cursor over all objects, which is checked in the CWindow::CheckMouseFocus() method when the cursor is moving over the chart. Earlier, two icons were loaded into objects that have a role of buttons here. They can be switched by setting their state using the CChartObjectBmpLabel::State() method. The CElement::ChangeObjectColor() method works with the color of the specified part of the object.

In this case, the color of the header background is to change when the cursor is in the window area. The color of the header frame or the background together with the frame or even the color of other objects can also be changed if the CElement::ChangeObjectColor() method is sequentially called with the name of the object specified for each of them in the first parameter.

The only thing left now is placing a call for this method in the local timer:

//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void CWindow::OnEventTimer(void)
  {
//--- Changing the color of the form objects
   ChangeObjectsColor();
  }

Compile all files of the project that were changed and load the program for tests on to the chart. Now, when the cursor is over an object on the form where functionality is implied, the color of the object will change.

Fig. 2. Test for the object reaction to the mouse cursor.

Fig. 2. Test for the object reaction to the mouse cursor

 

Conclusion

In this article, we introduced some additional changes, which allow to move a form over the chart. The controls of the form now react to the mouse cursor movement. In the next article, we will continue developing the CWindow class. It will be amplified with the methods, which will allow to manage the form through clicking on its controls.

You can download the material from the first part of the series so you can test how it works. If you have questions on using the material presented in those files, you can refer to the detailed description of the library development in one of the articles from the list below or ask your question in the comments of this article.

List of the articles (chapters) of the first part: