Русский 中文 Español Deutsch 日本語 Português
Graphics in DoEasy library (Part 81): Integrating graphics into library objects

Graphics in DoEasy library (Part 81): Integrating graphics into library objects

MetaTrader 5Examples | 20 September 2021, 13:45
7 805 0
Artyom Trishkin
Artyom Trishkin

Contents

Concept

This time I am not going to implement any new graphical constructions or improvements. Instead, I will start integrating the already created graphical element classes into library objects. This enables further development and improvement of graphical elements. Later, I will need to implement moving objects along a chart to control the validity of composite graphical objects. To achieve this, it is necessary to think over the integration of these objects into the library objects beforehand so that we are able to create the class for managing graphical objects, as well as their collection class.

The graphical object management class is to contain methods for creating forms and subsequent graphical objects that are to return the pointer to the created graphical object so that it is possible to work with it later. We will need the graphical element collection class later to create the lists of all built graphical objects related to different library objects so that it is possible to crate methods of their interaction with each other and with the program users.

In the current article, I will integrate graphical objects only into one of the library objects — the bar object. I will need some time to debug the created concept. Then I will add it to the remaining library objects with the help of the developed and debugged mechanism. After that, I am going to get back to the development of library graphical objects.

The current concept is as follows:

  • We have the graphical element object with different drawing methods;
  • we have the bar object which knows nothing about graphical objects yet;
  • we need to create the graphical object management class allowing us to create them and return the pointer to the created object;
  • we need to make this management class one of the bar object components.

These four steps allow us to get any previously created library object (currently, this is the bar object). The object for managing graphical objects enables us to create a necessary object and get the pointer to it so that it is possible to further work with it as with a usual library graphical object I considered in the previous articles.

After debugging the concept, I will implement everything I am currently doing to the bar object here to all library objects in order to integrate the graphical component of the library. All objects will get a new "visual" dimension, while we will get a new tool for interacting with the library.


Improving library classes

Each graphical object created by a library object should be aware of it. Of course, if we have a single object able to create graphical objects for itself (currently, this is a bar object), then the newly created graphical object does not need to know, which object created it. But if each library object is able to create graphical objects for itself, then all created graphical objects should know inside which object they are created so that it is possible to refer to the object that created that graphical object and get data from it. This can be useful for displaying the data on the graphical object or for achieving more complex relationships between different objects.

Of course, it is impossible to do all that within a single article. I will start from the simplest things. We need to know the description of the type of the object the graphical object has been created from. To achieve this, let's use the object collection ID (the ID of the collection corresponding to an object type is set for each object). The ID allows us to define the object type the library object (from which the graphical object has been created) belongs to. Of course, this is insufficient for the accurate indication of a specific object. But as I have already said, I am starting with simple things.

Besides, we will need the methods for displaying object descriptions of the same type for all previously created library objects. These are Print() and PrintShort() methods displaying the full and short descriptions of the object properties. Let's make these methods virtual and declare them in the parent class of all CBaseObj library objects. For the virtualization to work, we need to make the arguments of these methods exactly the same in all classes. At the moment, we have different parameters for these methods in different classes. It is necessary to bring them to a single form and correct the method calls in accordance with the changed parameters in the method arguments.

In the CBaseObj class in \MQL5\Include\DoEasy\Objects\BaseObj.mqh, declare the two virtual methods with the necessary parameters:

//--- Return an object type
   virtual int       Type(void)                                const { return this.m_type;                     }
//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false)  { return;                        }
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false){ return;                        }
   
//--- Constructor

The parameters in the method arguments are selected so that they can be used in all the methods I have previously implemented in the derived classes.

For example, the COrder class (the base class of the entire order system of the library) features the following changes:

//--- Return order/position direction
   string            DirectionDescription(void) const;
//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//---
  };
//+------------------------------------------------------------------+

Here I have added yet another argument to the Print() method and declared the PrintShort() method.

While implementing the method outside the class body, I have also added the additional method argument:

//+------------------------------------------------------------------+
//| Send order properties to the journal                             |
//+------------------------------------------------------------------+
void COrder::Print(const bool full_prop=false,const bool dash=false)
  {
   ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG),": \"",this.StatusDescription(),"\" =============");
   int beg=0, end=ORDER_PROP_INTEGER_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_INTEGER prop=(ENUM_ORDER_PROP_INTEGER)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   beg=end; end+=ORDER_PROP_DOUBLE_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_DOUBLE prop=(ENUM_ORDER_PROP_DOUBLE)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   beg=end; end+=ORDER_PROP_STRING_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_STRING prop=(ENUM_ORDER_PROP_STRING)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("================== ",CMessage::Text(MSG_LIB_PARAMS_LIST_END),": \"",this.StatusDescription(),"\" ==================\n");
  }
//+------------------------------------------------------------------+

Below is an example of improving method calls with the parameters implemented in the arguments:

//+------------------------------------------------------------------+
//| Display complete collection description to the journal           |
//+------------------------------------------------------------------+
void CMBookSeriesCollection::Print(const bool full_prop=false,const bool dash=false)
  {
   ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":");
   for(int i=0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
         continue;
      bookseries.Print(false,true);
     }
  }
//+------------------------------------------------------------------+
//| Display the short collection description in the journal          |
//+------------------------------------------------------------------+
void CMBookSeriesCollection::PrintShort(const bool dash=false,const bool symbol=false)
  {
   ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":");
   for(int i=0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
         continue;
      bookseries.PrintShort(true);
     }
  }
//+------------------------------------------------------------------+

There was a single parameter here previously and the method was called as bookseries.Print(true); Now, the Print() method of the CMBookSeries class features yet another parameter before the necessary one. Therefore, first we pass false for the added parameter, and then we pass true for the necessary one (the one previously present when calling the method).

Similar changes have affected almost all previously written classes of the library objects, and they have already been implemented into all classes that contain these methods and the ones inherited from the base object of all library objects: 

BookSeriesCollection.mqh, ChartObjCollection.mqh, MQLSignalsCollection.mqh, TickSeriesCollection.mqh, TimeSeriesCollection.mqh.

Account.mqh, MarketBookOrd.mqh, MarketBookSnapshot.mqh, MBookSeries.mqh, ChartObj.mqh, ChartWnd.mqh, MQLSignal.mqh, Order.mqh.

Buffer.mqh, BufferArrow.mqh, BufferBars.mqh, BufferCalculate.mqh, BufferCandles.mqh, BufferFilling.mqh, BufferHistogram.mqh, BufferHistogram2.mqh, BufferLine.mqh, BufferSection.mqh, BufferZigZag.mqh, DataInd.mqh, IndicatorDE.mqh.

PendReqClose.mqh, PendReqModify.mqh, PendReqOpen.mqh, PendReqPlace.mqh, PendReqRemove.mqh, PendReqSLTP.mqh, PendRequest.mqh.

Bar.mqh, SeriesDE.mqh, TimeSeriesDE.mqh, DataTick.mqh, TickSeries.mqh.

Symbol.mqh, SymbolBonds.mqh, SymbolCFD.mqh, SymbolCollateral.mqh, SymbolCommodity.mqh, SymbolCommon.mqh, SymbolCrypto.mqh, SymbolCustom.mqh, SymbolExchange.mqh, SymbolFutures.mqh, SymbolFX.mqh, SymbolFXExotic.mqh, SymbolFXMajor.mqh, SymbolFXMinor.mqh, SymbolFXRub.mqh, SymbolIndex.mqh, SymbolIndicative.mqh, SymbolMetall.mqh, SymbolOption.mqh, SymbolStocks.mqh

BaseObj.mqh.

In some classes of the library, messages have been replaced by the standard Print() function with displaying messages using the ToLog() method of the CMessage class like, for example, in the following method of the event collection class:

//+------------------------------------------------------------------+
//| Select only market pending orders from the list                  |
//+------------------------------------------------------------------+
CArrayObj* CEventsCollection::GetListMarketPendings(CArrayObj* list)
  {
   if(list.Type()!=COLLECTION_MARKET_ID)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_ERROR_NOT_MARKET_LIST);
      return NULL;
     }
   CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL);
   return list_orders;
  }
//+------------------------------------------------------------------+

The following string was previously used to display a message:

Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_NOT_MARKET_LIST));

The list of files featuring such edits:

EventsCollection.mqh, HistoryCollection.mqh, TimeSeriesCollection.mqh.

Find the changes in the attached files.

If the chart features the already created form object, it can be hidden or shown by specifying the display flags on specified timeframes. We will use that for "moving" the object to the foreground above all others using the BringToTop() method.
We do not have methods for showing/hiding graphical objects.
Let's create two virtual methods in the CGCnvElement graphical element class in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh:

//--- Set the object above all
   void              BringToTop(void)                          { CGBaseObj::SetVisible(false); CGBaseObj::SetVisible(true);            }
//--- (1) Show and (2) hide the element
   virtual void      Show(void)                                { CGBaseObj::SetVisible(true);                                          }
   virtual void      Hide(void)                                { CGBaseObj::SetVisible(false);                                         }

The methods simply set the appropriate flags for displaying the object on all timeframes in the base object of the library graphical objects.

The CForm form object class is a descendant of the graphical element object, and the form object can consist of several graphical element objects. Therefore, we need a separate implementation of these methods for it.
Open \MQL5\Include\DoEasy\Objects\Graph\Form.mqh and declare two virtual methods in the public section:

//+------------------------------------------------------------------+
//| Visual design methods                                            |
//+------------------------------------------------------------------+
//--- (1) Show and (2) hide the form
   virtual void      Show(void);
   virtual void      Hide(void);

//+------------------------------------------------------------------+
//| Methods of simplified access to object properties                |
//+------------------------------------------------------------------+

Let's write their implementation outside the class body:

//+------------------------------------------------------------------+
//| Show the form                                                    |
//+------------------------------------------------------------------+
void CForm::Show(void)
  {
//--- 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();
//--- 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 *elment=this.m_list_elements.At(i);
      if(elment==NULL)
         continue;
      //--- and display it
      elment.Show();
     }
//--- Update the form
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+
//| Hide the form                                                    |
//+------------------------------------------------------------------+
void CForm::Hide(void)
  {
//--- If the object has a shadow, hide it
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Hide();
//--- 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 *elment=this.m_list_elements.At(i);
      if(elment==NULL)
         continue;
      //--- and hide it
      elment.Hide();
     }
//--- Hide the main form and update the object
   CGCnvElement::Hide();
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Both methods are commented in detail in the code listing. In brief, when hiding objects, there is no much difference in the order they are hidden. When displaying objects, however, it is important to restore the entire sequence of the location of all bound objects on the main form. Therefore, the display takes place in layers — first, the lowest object is shown (the form shadow). The main form is then displayed above the shadow. All graphical elements bound to the main form are displayed afterwards. In this implementation, the display order corresponds to the order, in which they are added to the list of bound objects.
The algorithm is to be checked when creating complex (composite) form objects.

Now it is time to start the integration of graphical objects to the library objects.

Graphical object management class

So how can we endow each library object with the ability to create graphical objects for itself?

Most library objects are derived from the base object of all CBaseObj library objects. If we add an instance of the class having the ability to create all possible graphical objects (both available and planned for further development) and provide access to the pointer to the created object, then all its descendants will be able to work with graphical objects.

Since we will have a great number of different graphical objects, we need a class that "knows" of each such object and is able to create and manage them. This will be the class for managing graphical objects.

In \MQL5\Include\DoEasy\Objects\Graph\, create a new file GraphElmControl.mqh of the CGraphElmControl class. The class should be derived from the base class for constructing the CObject MQL5 standard library. The class listing should include three files — the file of the class of dynamic array of pointers to CObject class instances and its descendants, the service function file and the file of the form object class:

//+------------------------------------------------------------------+
//|                                              GraphElmControl.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\..\Services\DELib.mqh"
#include "Form.mqh"
//+------------------------------------------------------------------+
//| Class for managing graphical elements                            |
//+------------------------------------------------------------------+
class CGraphElmControl : public CObject
  {
private:
   int               m_type_node;                     // Type of the object the graphics is constructed for
public:
//--- Return itself
   CGraphElmControl *GetObject(void)                  { return &this;               }
//--- Set a type of the object the graphics is constructed for
   void              SetTypeNode(const int type_node) { this.m_type_node=type_node; }
   
//--- Create a form object
   CForm            *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            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h);
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h);

//--- Constructors
                     CGraphElmControl(){;}
                     CGraphElmControl(int type_node);
  };
//+------------------------------------------------------------------+

The m_type_node variable is to store the type of object containing the class object. When creating a new object (currently, it is a bar object), its constructor calls the SetTypeNode() method receiving a type of the bar object set into its m_type variable (in case of a bar, it is the bar object collection ID). Thus, the object of managing graphical objects knows of the class constructing its objects. For now, I will just use the collection ID. In the future, I will consider passing the pointer to the object the graphics is constructed from.

Let's consider the class methods.

In the parametric constructor of the class, set the object type passed in the method arguments to the m_type_node variable:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CGraphElmControl::CGraphElmControl(int type_node)
  {
   this.m_type_node=m_type_node;
  }
//+------------------------------------------------------------------+


The method creating the form object on a specified chart in a specified subwindow:

//+----------------------------------------------------------------------+
//| 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(chart_id,wnd,name,x,y,w,h);
   if(form==NULL)
      return NULL;
   form.SetID(form_id);
   form.SetNumber(0);
   return form;
  }
//+------------------------------------------------------------------+

The method receives a unique ID of the created form object, chart ID, chart subwindow index, form object name, X/Y coordinates for creating the form, as well as the form width and height.
Next, create a new form object with the parameters passed to the method. If it is created successfully, set the form ID and the index in the object list for the object (currently, it is zero since the object contains no other bound form objects and is the main form object). Return the pointer to a newly created object.

The method creating the form object on the current chart in a specified subwindow:

//+----------------------------------------------------------------------+
//| Create the form object on the current chart in a specified subwindow |
//+----------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
  {
   return this.CreateForm(form_id,::ChartID(),wnd,name,x,y,w,h);
  }
//+------------------------------------------------------------------+

The method receives a unique ID of the created form object, chart subwindow index, form object name, X/Y coordinates for creating the form, as well as the form width and height. The method returns the operation result of the above form for calling the method with an explicit indication of the current chart ID.

The method creating the form object on the current chart in the chart main window:

//+----------------------------------------------------------------------+
//| Create the form object on the current chart in the chart main window |
//+----------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h)
  {
   return this.CreateForm(form_id,::ChartID(),0,name,x,y,w,h);
  }
//+------------------------------------------------------------------+

The method receives a unique ID of the created form object, form object name, X/Y coordinates for creating the form, as well as the form width and height. The method returns the operation result of the first form of calling the method with an explicit indication of the current chart ID and the chart main window index.

We do not need anything else here in order to create form objects. All the work on creating various animations on the created form object will be carried out by the pointer to the object. The pointer is returned by the methods considered above.

We are ready to start integrating graphics handling into all library objects, which are descendants of the base object of all CBaseObj library objects.


Integrating graphics into the library

So, we need each library object to "see" the classes of graphical objects and be able to create these objects for themselves. To achieve this, we need to simply declare the instance of the class for managing graphical objects within the class of the base object of all library objects. All its descendants will be immediately endowed with the ability to create graphics by working via the CGraphElmControl class instance I have just considered.

Open \MQL5\Include\DoEasy\Objects\BaseObj.mqh and include the file of the graphical object management class to it:

//+------------------------------------------------------------------+
//|                                                      BaseObj.mqh |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\Services\DELib.mqh"
#include "..\Objects\Graph\GraphElmControl.mqh"
//+------------------------------------------------------------------+

In the protected section of the CBaseObj class, declare an instance of the graphical object management class object:

//+------------------------------------------------------------------+
//| Base object class for all library objects                        |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   CGraphElmControl  m_graph_elm;                              // Instance of the class for managing graphical elements
   ENUM_LOG_LEVEL    m_log_level;                              // Logging level
   ENUM_PROGRAM_TYPE m_program;                                // Program type
   bool              m_first_start;                            // First launch flag
   bool              m_use_sound;                              // Flag of playing the sound set for an object
   bool              m_available;                              // Flag of using a descendant object in the program
   int               m_global_error;                           // Global error code
   long              m_chart_id_main;                          // Control program chart ID
   long              m_chart_id;                               // Chart ID
   string            m_name;                                   // Object name
   string            m_folder_name;                            // Name of the folder storing CBaseObj descendant objects 
   string            m_sound_name;                             // Object sound file name
   int               m_type;                                   // Object type (corresponds to the collection IDs)

public:

Write the methods for creating a form object in the public section of the class:

//--- Return an object type
   virtual int       Type(void)                                const { return this.m_type;                     }
//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false)  { return;                        }
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false){ return;                        }
//--- Create a form object on a specified chart in a specified subwindow
   CForm            *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)
                       { return this.m_graph_elm.CreateForm(form_id,chart_id,wnd,name,x,y,w,h);                }
//--- Create a form object on the current chart in a specified subwindow
   CForm            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,wnd,name,x,y,w,h);                         }
//--- Create the form object on the current chart in the main window
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,name,x,y,w,h);                             }
   
//--- Constructor

The methods simplify the operation result of the three same-name methods of the graphical object management class considered above.

Now each of the descendant objects of the CBaseObj class is able to create a form object when calling the methods.

Today I will check handling graphical objects using the "Bar" object class.
Open the file of the class \MQL5\Include\DoEasy\Objects\Series\Bar.mqh and add passing the bar object type to the class of managing graphical objects in the SetProperties() method:

//+------------------------------------------------------------------+
//| Set bar object parameters                                        |
//+------------------------------------------------------------------+
void CBar::SetProperties(const MqlRates &rates)
  {
   this.SetProperty(BAR_PROP_SPREAD,rates.spread);
   this.SetProperty(BAR_PROP_VOLUME_TICK,rates.tick_volume);
   this.SetProperty(BAR_PROP_VOLUME_REAL,rates.real_volume);
   this.SetProperty(BAR_PROP_TIME,rates.time);
   this.SetProperty(BAR_PROP_TIME_YEAR,this.TimeYear());
   this.SetProperty(BAR_PROP_TIME_MONTH,this.TimeMonth());
   this.SetProperty(BAR_PROP_TIME_DAY_OF_YEAR,this.TimeDayOfYear());
   this.SetProperty(BAR_PROP_TIME_DAY_OF_WEEK,this.TimeDayOfWeek());
   this.SetProperty(BAR_PROP_TIME_DAY,this.TimeDay());
   this.SetProperty(BAR_PROP_TIME_HOUR,this.TimeHour());
   this.SetProperty(BAR_PROP_TIME_MINUTE,this.TimeMinute());
//---
   this.SetProperty(BAR_PROP_OPEN,rates.open);
   this.SetProperty(BAR_PROP_HIGH,rates.high);
   this.SetProperty(BAR_PROP_LOW,rates.low);
   this.SetProperty(BAR_PROP_CLOSE,rates.close);
   this.SetProperty(BAR_PROP_CANDLE_SIZE,this.CandleSize());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_BODY,this.BodySize());
   this.SetProperty(BAR_PROP_CANDLE_BODY_TOP,this.BodyHigh());
   this.SetProperty(BAR_PROP_CANDLE_BODY_BOTTOM,this.BodyLow());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_UP,this.ShadowUpSize());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_DOWN,this.ShadowDownSize());
//---
   this.SetProperty(BAR_PROP_TYPE,this.BodyType());
//--- Set the object type to the object of the graphical object management class
   this.m_graph_elm.SetTypeNode(this.m_type);
  }
//+------------------------------------------------------------------+

Almost everything is ready for testing. But there is one caveat. When I started working with graphical objects, I did not include them to the main library simply using graphical element classes "as is". Now we need to do everything right — all library objects are accessible through its main object — the CEngine class, to which the collection files of all objects are connected. But the graphical objects feature no class of their collection yet since not all objects are created. But we are able to create a preliminary class collection of graphical objects. After creating all objects, I will get back to it.

With these considerations in mind, I will create the preliminary version of the graphical object collection class. We will need to specify the ID of the graphical object collection list in \MQL5\Include\DoEasy\Defines.mqh:

//--- Parameters of the chart collection timer
#define COLLECTION_CHARTS_PAUSE        (500)                      // Chart collection timer pause in milliseconds
#define COLLECTION_CHARTS_COUNTER_STEP (16)                       // Chart timer counter increment
#define COLLECTION_CHARTS_COUNTER_ID   (9)                        // Chart timer counter ID
//--- Collection list IDs
#define COLLECTION_HISTORY_ID          (0x777A)                   // Historical collection list ID
#define COLLECTION_MARKET_ID           (0x777B)                   // Market collection list ID
#define COLLECTION_EVENTS_ID           (0x777C)                   // Event collection list ID
#define COLLECTION_ACCOUNT_ID          (0x777D)                   // Account collection list ID
#define COLLECTION_SYMBOLS_ID          (0x777E)                   // Symbol collection list ID
#define COLLECTION_SERIES_ID           (0x777F)                   // Timeseries collection list ID
#define COLLECTION_BUFFERS_ID          (0x7780)                   // Indicator buffer collection list ID
#define COLLECTION_INDICATORS_ID       (0x7781)                   // Indicator collection list ID
#define COLLECTION_INDICATORS_DATA_ID  (0x7782)                   // Indicator data collection list ID
#define COLLECTION_TICKSERIES_ID       (0x7783)                   // Tick series collection list ID
#define COLLECTION_MBOOKSERIES_ID      (0x7784)                   // DOM series collection list ID
#define COLLECTION_MQL5_SIGNALS_ID     (0x7785)                   // MQL5 signals collection list ID
#define COLLECTION_CHARTS_ID           (0x7786)                   // Chart collection list ID
#define COLLECTION_CHART_WND_ID        (0x7787)                   // Chart window list ID
#define COLLECTION_GRAPH_OBJ_ID        (0x7788)                   // Graphical object collection list ID
//--- Pending request type IDs

In the \MQL5\Include\DoEasy\Collections\ library folder, create a new file GraphElementsCollection.mqh of the CGraphElementsCollection class:

//+------------------------------------------------------------------+
//|                                      GraphElementsCollection.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Services\Select.mqh"
#include "..\Objects\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Collection of graphical objects                                  |
//+------------------------------------------------------------------+
class CGraphElementsCollection : public CBaseObj
  {
private:
   CListObj          m_list_all_graph_obj;      // List of all graphical objects
   int               m_delta_graph_obj;         // Difference in the number of graphical objects compared to the previous check
//--- Return the flag indicating the graphical element object in the list of graphical objects
   bool              IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj);
public:
//--- Return itself
   CGraphElementsCollection *GetObject(void)                                                             { return &this;                        }
   //--- Return the full collection list 'as is'
   CArrayObj        *GetList(void)                                                                       { return &this.m_list_all_graph_obj;   }
   //--- Return the list by selected (1) integer, (2) real and (3) string properties meeting the compared criterion
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   //--- Return the number of new graphical objects
   int               NewObjects(void)   const                                                            { return this.m_delta_graph_obj;       }
   //--- Constructor
                     CGraphElementsCollection();
//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CGraphElementsCollection::CGraphElementsCollection()
  {
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
   this.m_list_all_graph_obj.Clear();
   this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID);
  }
//+------------------------------------------------------------------+

The class structure is similar to the collection class structure of other library objects. The only thing that matters here is the class constructor — all other methods match the ones in other library object collections. We will implement them later. Currently, it is important that the class contains the form object class file allowing library-based programs to see graphical objects. The class constructor for the current chart enables tracking mouse movement and mouse wheel scrolling events.

All is set. The remaining things will be handled later — after creating all library graphical objects.

It only remains to include the file of the graphical object collection class to the CEngine library main object file located in \MQL5\Include\DoEasy\Engine.mqh:

//+------------------------------------------------------------------+
//|                                                       Engine.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "Services\TimerCounter.mqh"
#include "Collections\HistoryCollection.mqh"
#include "Collections\MarketCollection.mqh"
#include "Collections\EventsCollection.mqh"
#include "Collections\AccountsCollection.mqh"
#include "Collections\SymbolsCollection.mqh"
#include "Collections\ResourceCollection.mqh"
#include "Collections\TimeSeriesCollection.mqh"
#include "Collections\BuffersCollection.mqh"
#include "Collections\IndicatorsCollection.mqh"
#include "Collections\TickSeriesCollection.mqh"
#include "Collections\BookSeriesCollection.mqh"
#include "Collections\MQLSignalsCollection.mqh"
#include "Collections\ChartObjCollection.mqh"
#include "Collections\GraphElementsCollection.mqh"
#include "TradingControl.mqh"
//+------------------------------------------------------------------+
//| Library basis class                                              |
//+------------------------------------------------------------------+
class CEngine
  {

Now we are ready to test integrated graphical objects to the bar object class.


Test

Let's create the timeseries list for the current symbol and chart period. The list stores bar objects. These objects now feature the class for managing graphical objects, which allows creating a separate form object for each bar.

Let's do it this way: when pressing and holding Ctrl and hovering the mouse over the chart, a form object featuring a shadow and a bar type description (bullish/bearish/doji) is created for a bar the mouse cursor is located on. As soon as we press and hold Ctrl, the ability to scroll the chart with the mouse and the wheel is disabled, and the form with the bar description is displayed immediately. After Ctrl is released, the list of created form objects is cleared upon a new tick arrival or when shifting the chart since tracking the Ctrl key holding/releasing moments is not required for the test. Even clearing the list of created objects is necessary only for "hiding" some issues that arise when changing the chart scale. The previously created form objects are displayed in their old places, which means they no longer correspond to the current candle position on the changed chart scale. For the sake of a test speed, it is easier to clear the list than recalculate the object coordinates when changing the chart scale.

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

In the global area, instead of including files,

#include <Arrays\ArrayObj.mqh>
#include <DoEasy\Services\Select.mqh>
#include <DoEasy\Objects\Graph\Form.mqh>

include the file of the main library object and declare its instance:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart81.mq5 |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- defines
#define        FORMS_TOTAL (4)   // Number of created forms
#define        START_X     (4)   // Initial X coordinate of the shape
#define        START_Y     (4)   // Initial Y coordinate of the shape
//--- input parameters
sinput   bool              InpMovable     =  true;          // Movable forms flag
sinput   ENUM_INPUT_YES_NO InpUseColorBG  =  INPUT_YES;     // Use chart background color to calculate shadow color
sinput   color             InpColorForm3  =  clrCadetBlue;  // Third form shadow color (if not background color) 
//--- global variables
CEngine        engine;
CArrayObj      list_forms;  
color          array_clr[];

//+------------------------------------------------------------------+

From the EA's OnInit() handler, delete the code creating form objects. We only need to specify a used symbol in the library and create the timeseries for the current symbol and period. As a result, the handler looks as follows:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set the permissions to send cursor movement and mouse scroll events
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
//--- Set EA global variables
   ArrayResize(array_clr,2);        // Array of gradient filling colors
   array_clr[0]=C'246,244,244';     // Original ≈pale gray
   array_clr[1]=C'249,251,250';     // Final ≈pale gray-green
//--- Create the array with the current symbol and set it to be used in the library
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Remove the FigureType() and FigureProcessing() functions from the EA. We do not need them for this test, while they take almost entire EA code volume.

Replace them with three functions.

The function returning the flag that indicates the existence of a form with a specified name:

//+-------------------------------------------------------------------------------+
//| Return the flag that indicates the existence of a form with a specified name  |
//+-------------------------------------------------------------------------------+
bool IsPresentForm(const string name)
  {
   //--- In the loop by the list of form objects,
   for(int i=0;i<list_forms.Total();i++)
     {
      //--- get the next form object
      CForm *form=list_forms.At(i);
      if(form==NULL)
         continue;
      //--- form the desired object name as "Program name_" + the form name passed to the function
      string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name;
      //--- If the current form object has such a name, return 'true'
      if(form.NameObj()==nm)
         return true;
     }
   //--- Upon the loop completion, return 'false'
   return false;
  }
//+------------------------------------------------------------------+

The function hiding all forms except the one with the specified name:

//+------------------------------------------------------------------+
//| Hide all forms except the one with the specified name            |
//+------------------------------------------------------------------+
void HideFormAllExceptOne(const string name)
  {
   //--- In the loop by the list of form objects,
   for(int i=0;i<list_forms.Total();i++)
     {
      //--- get the next form object
      CForm *form=list_forms.At(i);
      if(form==NULL)
         continue;
      //--- form the desired object name as "Program name_" + the form name passed to the function
      string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name;
      //--- If the current form object has such a name, display it,
      if(form.NameObj()==nm)
         form.Show();
      //--- otherwise - hide
      else
         form.Hide();
     }
  }
//+------------------------------------------------------------------+

The function returning the flag of holding Ctrl:

//+------------------------------------------------------------------+
//| Return the flag of holding Ctrl                                  |
//+------------------------------------------------------------------+
bool IsCtrlKeyPressed(void)
  {
   return((TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)&0x80)!=0);
  }
//+------------------------------------------------------------------+

All functions are quite simple. I believe, they require no explanations.

Remove handling keystrokes and object clicks from the OnChartEvent() handler. We will need it here. Add handling mouse movements:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If the mouse is moved
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      CForm *form=NULL;
      datetime time=0;
      double price=0;
      int wnd=0;
      
      //--- If Ctrl is not pressed,
      if(!IsCtrlKeyPressed())
        {
         //--- clear the list of created form objects and allow scrolling a chart with the mouse
         list_forms.Clear();
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true);
         return;
        }
      
      //--- If X and Y chart coordinates are successfully converted into time and price,
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- get the bar index the cursor is hovered over
         int index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         if(index==WRONG_VALUE)
            return;
         
         //--- Get the bar index by index
         CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index);
         if(bar==NULL)
            return;
         
         //--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates
         int x=(int)lparam,y=(int)dparam;
         if(!ChartTimePriceToXY(ChartID(),0,bar.Time(),(bar.Open()+bar.Close())/2.0,x,y))
            return;
         
         //--- Disable moving a chart with the mouse
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,false);
         
         //--- Create the form object name and hide all objects except one having such a name
         string name="FormBar_"+(string)index;
         HideFormAllExceptOne(name);
         
         //--- If the form object with such a name does not exist yet,
         if(!IsPresentForm(name))
           {
            //--- create a new form object
            form=bar.CreateForm(index,name,x,y,76,16);
            if(form==NULL)
               return;
            
            //--- Set activity and unmoveability flags for the form
            form.SetActive(true);
            form.SetMovable(false);
            //--- Set the opacity of 200
            form.SetOpacity(200);
            //--- The form background color is set as the first color from the color array
            form.SetColorBackground(array_clr[0]);
            //--- Form outlining frame color
            form.SetColorFrame(C'47,70,59');
            //--- Draw the shadow drawing flag
            form.SetShadow(true);
            //--- Calculate the shadow color as the chart background color converted to the monochrome one
            color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
            //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units
            //--- Otherwise, use the color specified in the settings for drawing the shadow
            color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
            //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes
            //--- Set the shadow opacity to 200, while the blur radius is equal to 4
            form.DrawShadow(2,2,clr,200,3);
            //--- Fill the form background with a vertical gradient
            form.Erase(array_clr,form.Opacity());
            //--- Draw an outlining rectangle at the edges of the form
            form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
            //--- If failed to add the form object to the list, remove the form and exit the handler
            if(!list_forms.Add(form))
              {
               delete form;
               return;
              }
            //--- Capture the form appearance
            form.Done();
           }
         //--- If the form object exists,
         if(form!=NULL)
           {
            //--- draw a text with the bar type description on it and show the form. The description corresponds to the mouse cursor position
            form.TextOnBG(0,bar.BodyTypeDescription(),form.Width()/2,form.Height()/2-1,FRAME_ANCHOR_CENTER,C'7,28,21');
            form.Show();
           }
         //--- Redraw the chart
         ChartRedraw();
        }
     }
  }
//+------------------------------------------------------------------+

The handler code features detailed comments in the listing. I hope, everything is clear there. If you have any questions, feel free to ask them in the comments.

Compile the EA and launch it on the chart. Press and hold Ctrl and move the mouse over the chart. Each bar is to have its form object containing the bar type description (bearish/bullish/doji). Releasing Ctrl removes all created objects.



What's next?

In the next article, I will continue the integration of graphical objects to the library objects.

All files of the current version of the library are attached below together with the test EA file for MQL5 for you to test and download.
Leave your questions and suggestions in the comments.

Back to contents

*Previous articles within the series:

Graphics in DoEasy library (Part 73): Form object of a graphical element
Graphics in DoEasy library (Part 74): Basic graphical element powered by the CCanvas class
Graphics in DoEasy library (Part 75): Methods of handling primitives and text in the basic graphical element
Graphics in DoEasy library (Part 76): Form object and predefined color themes
Graphics in DoEasy library (Part 77): Shadow object class
Graphics in DoEasy library (Part 78): Animation principles in the library. Image slicing
Graphics in DoEasy library (Part 79): "Animation frame" object class and its descendant objects
Graphics in DoEasy library (Part 80): "Geometric animation frame" object class

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/9751

Attached files |
MQL5.zip (4087.48 KB)
Combinatorics and probability theory for trading (Part II): Universal fractal Combinatorics and probability theory for trading (Part II): Universal fractal
In this article, we will continue to study fractals and will pay special attention to summarizing all the material. To do this, I will try to bring all earlier developments into a compact form which would be convenient and understandable for practical application in trading.
Better programmer (Part 05): How to become a faster developer Better programmer (Part 05): How to become a faster developer
Every developer wants to be able to write code faster, and being able to code faster and effective is not some kind of special ability that only a few people are born with. It's a skill that can be learned, that is what I'm trying to teach in this article.
Graphics in DoEasy library (Part 82): Library objects refactoring and collection of graphical objects Graphics in DoEasy library (Part 82): Library objects refactoring and collection of graphical objects
In this article, I will improve all library objects by assigning a unique type to each object and continue the development of the library graphical objects collection class.
Graphics in DoEasy library (Part 80): "Geometric animation frame" object class Graphics in DoEasy library (Part 80): "Geometric animation frame" object class
In this article, I will optimize the code of classes from the previous articles and create the geometric animation frame object class allowing us to draw regular polygons with a given number of vertices.