Graphical Interfaces II: the Separation Line and Context Menu Elements (Chapter 2)

Anatoli Kazharski | 11 March, 2016

Contents  


 

Introduction

The first article Graphical Interfaces I: Preparation of the Library Structure (Chapter 1) considers in detail what this library is for. A full list of the links to the articles is at the end of each chapter of the series. There you can also find and download a complete version of the library at the current stage of development. The files must be located in the same directories as in the archive.  

In the previous chapter, we wrote a class for creating a menu item. This is used both as an independent control and as a part of a context and main menu. In this article, we will describe the creation of the separation line element, which also can be used not only as an independent interface element but as a part of many other elements too. After that, we will have everything required for the development of the context menu class, which will also be discussed in this article in detail. Added to that, we will introduce all necessary additions to the class, which is the base for storing pointers to all the elements of the graphical interface of the application.



Developing the Class for Creating a Separation Line

In a context menu, besides different types of menu items, we can often see one more interface element - a separation line. This element can be encountered not only in context menus. For instance, the status bar of the MetaTrader trading terminal and the MetaEditor code editor have vertical separation lines. That is why we will create a separate class for this object so it can be used in any other control or even used as a separate element of the graphical interface.

To give an illusion of volume, a separation line must consist of at least two parts. If one line is lighter than the background and the other one is darker, this will create a visual effect of a groove on the surface. There are two ways of creating a separation line: (1) from two primitive objects of the CRectLabel type, which are already present in the Objects.mqh file or (2) create an object of the OBJ_BITMAP_LABEL type and use it as a canvas for drawing. Let us use the second option. The standard library suggests the CCanvas class for drawing. This class already contains all necessary methods for drawing simple geometrical figures, which significantly simplifies the implementation of the intended design and will save us a lot of time. 

The CCanvas class has to be embedded in a way so it can be used in a similar way to those primitive objects, which are already present in the Objects.mqh file. This can be easily achieved by making the CCanvas class derived from the CChartObjectBmpLabel class. A little change has to be introduced in the code of the CCanvas class so there are no errors or warnings later when the program is compiled. This is because in both the CCanvas class and the CChartObject class, which is the base class for the CChartObjectBmpLabel class, there is the m_chart_id field (variable). As a result, the compiler will give a warning that a variable with such a name already exists:

Fig. 1. Warning from the compiler that a variable with such a name already exists.

Fig. 1. Warning from the compiler

 

In practice, such a warning will not cause any critical errors and compilation will take place in spite of this. It is recommended, however, to avoid such situations because its potential impact on the work of the program is unknown. Let us assume this as a rule and follow it. Besides, the changes in the Canvas.mqh file will have to be introduced anyway because the CCanvas class has to be derived from the CChartObjectBmpLabel class. This way we can easily get rid of the persisting warning. We will simply remove the m_chart_id variable from the CCanvas class. Introducing changes in the classes of the standard library, it must be noted that at the next terminal update the files of the standard library can also be updated and the changes will get nullified. Therefore, as we cannot achieve our goal without making changes in the CCanvas class, create its copy and place it in the directory where the files of our library are located.

Create the Canvas folder in the #Include folder. Create a copy of the file containing the СCanvas class and rename it to CustomCanvas.mqh and name the class CCustomCanvas. Include the ChartObjectsBmpControls.mqh file of the standard library in the CustomCanvas.mqh file and make the CCustomCanvas class a derived one from the CChartObjectBmpLabel class. Then remove the m_chart_id variable from the body of the CCustomCanvas class and from the constructor.

//+------------------------------------------------------------------+
//|                                                 CustomCanvas.mqh |
//|                   Copyright 2009-2013, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Files\FileBin.mqh>
#include <Controls\Rect.mqh>
#include <ChartObjects\ChartObjectsBmpControls.mqh>

//...

//+------------------------------------------------------------------+
//| Class CCustomCanvas                                              |
//| Usage: class for working with a dynamic resource                 |
//+------------------------------------------------------------------+
class CCustomCanvas : public CChartObjectBmpLabel
  {
//...

Now, include the CustomCanvas.mqh file in the Objects.mqh file:

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "..\Canvas\CustomCanvas.mqh"
#include <ChartObjects\ChartObjectsBmpControls.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

Then, let us create the CRectCanvas class, which has to be a derived class from the CCustomCanvas class. The CRectCanvas class will be similar to all other classes, which are in the Objects.mqh file. Their contents were considered in the previous article. Now, it can be used for drawing any other interface elements, which will be a part of the library under development. 

We have everything ready for the development of the CSeparateLine class, which is intended for the creation of a separation line. Create the SeparateLine.mqh file in the Controls folder. Include the Element.mqh and Window.mqh files in it. Then, follow the following sequence of steps:

1) create the CSeparateLine class;

2) in this class, declare a pointer to the form to which the element is going to be attached and create a method for storing a pointer in it;

3) create an instance of the CRectCanvas class;

4) declare and implement standard visual methods for all elements that can be used to manage the element.

//+------------------------------------------------------------------+
//|                                                 SeparateLine.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Class for creating a separation line                             |
//+------------------------------------------------------------------+
class CSeparateLine : public CElement
  {
private:
   //--- Pointer to the form to which the element is attached
   CWindow          *m_wnd;
   //--- Object for creating a separation line
   CRectCanvas       m_canvas;
   //---
public:
                     CSeparateLine(void);
                    ~CSeparateLine(void);
   //--- Stores the pointer to the passed form
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
   //---
public:
   //--- Chart event handler
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Moving the element
   virtual void      Moving(const int x,const int y);
   //--- (1) Show, (2) hide, (3) reset, (4) delete
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void)
  {
//--- Store the name of the element class in the base class  
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSeparateLine::~CSeparateLine(void)
  {
  }
//+------------------------------------------------------------------+

To set up the appearance of the separation line, let us create three methods. These methods will be used for setting:

The enumeration, which will be used for specifying the type, has to be added to the Enums.mqh file:

//+------------------------------------------------------------------+
//| Enumeration of the separation line types                         |
//+------------------------------------------------------------------+
enum ENUM_TYPE_SEP_LINE
  {
   H_SEP_LINE =0,
   V_SEP_LINE =1
  };

Now, we can add the corresponding variables and methods to the CSeparateLine class and carry out the initialization by default values in the constructor:

class CSeparateLine : public CElement
  {
private:
   //--- Properties
   ENUM_TYPE_SEP_LINE m_type_sep_line;   
   color             m_dark_color;
   color             m_light_color;
   //---
public:
   //--- (1) Line type, (2) line colors
   void              TypeSepLine(const ENUM_TYPE_SEP_LINE type) { m_type_sep_line=type; }
   void              DarkColor(const color clr)                 { m_dark_color=clr;     }
   void              LightColor(const color clr)                { m_light_color=clr;    }
   //---
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void) : m_type_sep_line(H_SEP_LINE),
                                     m_dark_color(clrBlack),
                                     m_light_color(clrDimGray)
  {
  }

We only have to add the methods for creating the element and the method, in which a separation line will be drawn on the canvas. The following parameters must be passed to the public method for creating the element CreateSeparateLine(), which will be called in the custom application:

class CSeparateLine : public CElement
  {
public:
   //--- Creating a separation line
   bool              CreateSeparateLine(const long chart_id,const int subwin,const int index,
                                        const int x,const int y,const int x_size,const int y_size);
   //---
private:
   //--- Creates the canvas for drawing a separation line
   bool              CreateSepLine(void);
   //--- Drawing a separation line
   void              DrawSeparateLine(void);
   //---
  };

The code of the CreateSeparateLine() method does not differ from other similar methods in principle (for instance, in the CMenuItem class), that is why we will next consider the code of the CreateSepLine() method. 

Similar to all other methods of this type, the name of the graphical object is set at the beginning. Then the graphical object (canvas) is created on which we will draw. It should be noted that for the creation of the object of the OBJ_BITMAP_LABEL type, the CreateBitmapLabel() method is used, which belongs to the CCustomCanvas class. In this class, when objects are created, attaching the object to the chart is not provided as in the CChartObjectBmpLabel class, where the CChartObject::Attach() method of the base class is used straight after creation of the object. We ourselves must manage this. As the CCustomCanvas class was derived from the CChartObjectBmpLabel class, we can access the CChartObject::Attach() method from its base class. If the object is not attached to the chart, it will be impossible to manage.

After the object was created, attached to the chart and the properties have been set up, a separation line can be drawn on our custom canvas using the DrawSeparateLine() method. This is shown in the code below. Then the object pointer is stored in the array of the CElement base class.

//+------------------------------------------------------------------+
//| Creates the canvas for drawing a separation line                 |
//+------------------------------------------------------------------+
bool CSeparateLine::CreateSepLine(void)
  {
//--- Forming the object name  
   string name=CElement::ProgramName()+"_separate_line_"+(string)CElement::Index()+"__"+(string)CElement::Id();
//--- Creating an object
   if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,m_x,m_y,m_x_size,m_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
      return(false);
//--- Attaching to the chart
   if(!m_canvas.Attach(m_chart_id,name,m_subwin,1))
      return(false);
//--- Properties
   m_canvas.Background(false);
//--- Margins from the edge point
   m_canvas.XGap(m_x-m_wnd.X());
   m_canvas.YGap(m_y-m_wnd.Y());
//--- Draw a separation line
   DrawSeparateLine();
//--- Add to array
   CElement::AddToArray(m_canvas);
   return(true);
  }

The code of the DrawSeparateLine() method is simple. At first, get the size of the canvas. Then clear the canvas by using the CCustomCanvas::Erase() method. Then, depending on whether a horizontal or vertical line has to be drawn, the program will move to the corresponding code block. As an example, we will describe building a horizontal line. At first, coordinates for two points of the line are defined and then the first line is drawn in the upper part of the canvas. Coordinates for the second line are defined in the lower part of the canvas. If the height of the canvas is set to be two pixels, then the lines will be located very close to each other. You can arrange a margin between the upper and the lower lines by setting the height of the canvas greater than two pixels. To display the changes, the canvas must be refreshed at the very end of the method using the CCustomCanvas::Update() method.

//+------------------------------------------------------------------+
//|  Draws a separation line                                         |
//+------------------------------------------------------------------+
void CSeparateLine::DrawSeparateLine(void)
  {
//--- Coordinates for the lines
   int x1=0,x2=0,y1=0,y2=0;
//--- Canvas size
   int   x_size =m_canvas.X_Size()-1;
   int   y_size =m_canvas.Y_Size()-1;
//--- Clear canvas
   m_canvas.Erase(::ColorToARGB(clrNONE,0));
//--- If the line is horizontal
   if(m_type_sep_line==H_SEP_LINE)
     {
      //--- The dark line above
      x1=0;
      y1=0;
      x2=x_size;
      y2=0;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- The light line below
      x1=0;
      x2=x_size;
      y1=y_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- If the line is vertical
   else
     {
      //--- The dark line on the left
      x1=0;
      x2=0;
      y1=0;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- The light line on the right
      x1=x_size;
      y1=0;
      x2=x_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- Refreshing canvas
   m_canvas.Update();
  }

 



Test of Attaching a Separation Line

Now, we can test how this works. In the previous article, we attached a menu item to the form. Following the same principle, a separation line can be attached as a separate interface element. 

I will briefly remind you about the process of attaching an element to the form.

If everything is done correctly, then after the program compilation and loading it on to the chart, the result should be as follows:

Fig. 2. Test of the separation line element.

Fig. 2. Test of the separation line element.


The development of the CSeparateLine class is finished, and we have everything ready to start implementing the class for creating a context menu.

 


Developing the Class for Creating a Context Menu

Previously, in the process of the library development, three interface elements were created: (1) a form for controls (CWindow), (2) the menu item control (CMenuItem) and (3) the separation line element (CSeparateLine). Each of them belongs to a simple type of element, as they are composed only of primitive objects. A context menu is classed as a complex (compound) type of control. It will be composed not only of primitive objects but also of other elements. The base class for those elements is CElement

Create the ContextMenu.mqh file in the Controls folder in the directory of our library. In this file include the files that will be used for creating the context menu:

//+------------------------------------------------------------------+
//|                                                  ContextMenu.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "MenuItem.mqh"
#include "SeparateLine.mqh"

Then, create the CContextMenu class with the standard set of virtual methods for all library elements, the form pointer and the method for storing it. We need an object of the OBJ_RECTANGLE_LABEL type as the element background. That is why to create this object, we are going to use the CRectLabel class from the Object.mqhfile. The CMenuItem class for menu items was created in the previous article. As a context menu usually contains more than one CMenuItem class and the number of these classes is not known initially, then a dynamic array of the instances of this class has to be declared. The same applies to the separation lines (CSeparateLine) of a context menu.

//+------------------------------------------------------------------+
//| Class for creating a context menu                                |
//+------------------------------------------------------------------+
class CContextMenu : public CElement
  {
private:
   //--- Pointer to the form to which the element is attached
   CWindow          *m_wnd;
   //--- Objects for creating a menu item
   CRectLabel        m_area;
   CMenuItem         m_items[];
   CSeparateLine     m_sep_line[];
   //---
public:
                     CContextMenu(void);
                    ~CContextMenu(void);
   //--- Stores the pointer to the passed form
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }

   //--- Chart event handler
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Timer
   virtual void      OnEventTimer(void);
   //--- Moving the element
   virtual void      Moving(const int x,const int y);
   //--- (1) Show, (2) hide, (3) reset, (4) delete
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Setting, (2) resetting of priorities for left clicking
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void)
  {
//--- Store the name of the element class in the base class
   CElement::ClassName(CLASS_NAME);
//--- Context menu is a drop-down element
   CElement::m_is_dropdown=true;
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CContextMenu::~CContextMenu(void)
  {
  }
//+------------------------------------------------------------------+

To set up the appearance of the context menu, corresponding fields and methods are required:

class CContextMenu : public CElement
  {
private:
   //--- Background properties
   int               m_area_zorder;
   color             m_area_color;
   color             m_area_border_color;
   color             m_area_color_hover;
   color             m_area_color_array[];
   //--- Properties of menu items
   int               m_item_y_size;
   color             m_item_back_color;
   color             m_item_border_color;
   color             m_item_back_color_hover;
   color             m_item_back_color_hover_off;
   color             m_label_color;
   color             m_label_color_hover;
   string            m_right_arrow_file_on;
   string            m_right_arrow_file_off;
   //--- Separation line properties
   color             m_sepline_dark_color;
   color             m_sepline_light_color;
   //---
public:
   //--- Number of menu items
   int               ItemsTotal(void)                         const { return(::ArraySize(m_items));         }
   //--- Methods for setting up the appearance of the context menu:
   //    Color of the context menu background
   void              MenuBackColor(const color clr)                 { m_area_color=clr;                     }
   void              MenuBorderColor(const color clr)               { m_area_border_color=clr;              }
   //--- (1) Height, (2) background color and (3) color of the menu item frame 
   void              ItemYSize(const int y_size)                    { m_item_y_size=y_size;                 }
   void              ItemBackColor(const color clr)                 { m_item_back_color=clr;                }
   void              ItemBorderColor(const color clr)               { m_item_border_color=clr;              }
   //--- Background color of (1) the available and (2) the blocked menu item when hovering the mouse cursor over it
   void              ItemBackColorHover(const color clr)            { m_item_back_color_hover=clr;          }
   void              ItemBackColorHoverOff(const color clr)         { m_item_back_color_hover_off=clr;      }
   //--- (1) Standard and (2) in-focus text color 
   void              LabelColor(const color clr)                    { m_label_color=clr;                    }
   void              LabelColorHover(const color clr)               { m_label_color_hover=clr;              }
   //--- Defining an icon for indicating the presence of a context menu in the item
   void              RightArrowFileOn(const string file_path)       { m_right_arrow_file_on=file_path;      }
   void              RightArrowFileOff(const string file_path)      { m_right_arrow_file_off=file_path;     }
   //--- (1) Dark and (2) light color of the separation line
   void              SeparateLineDarkColor(const color clr)         { m_sepline_dark_color=clr;             }
   void              SeparateLineLightColor(const color clr)        { m_sepline_light_color=clr;            }
   //---
  };

The menu item and its context menu must be connected, otherwise it will be impossible to manage those elements correctly. To be precise, the context menu and its items must have access to the menu item to which it is attached, that is to the previous node. This means that both the CContextMenu class and the CMenuItem class must have a pointer of the CMenuItem type and methods to store and get this pointer. This pointer will also be used for checking the sequence of creating the graphical interface of the program for correctness. This will be shown later, when the methods for creating a context menu are considered. 

Adding the pointer and methods for storing it and getting it in the CContextMenu class:

class CContextMenu : public CElement
  {
private:
   //--- Pointer to the previous node
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Getting and storing the pointer to the previous node
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

Same must be added to the CMenuItem class:

class CMenuItem : public CElement
  {
private:
   //--- Pointer to the previous node
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Getting and storing the pointer to the previous node
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

The graphical interface will be built in the custom class of the application (CProgram). When creating a context menu, we will require a method to specify the intended number of items in a context menu and unique values of some parameters not common for all the items. Let us write the CContextMenu::AddItem() method, which will accept as parameters: (1) the menu item text, (2) a path to the icon for the label of the available item, (3) a path to the icon for the blocked item and (4) the menu item type. Arrays to store passed values will also be required. The size of these arrays will be increased by one element each time when the CContextMenu::AddItem() method is called.

class CContextMenu : public CElement
  {
private:
   //--- Arrays of the menu item properties:
   //    (1) Text, (2) label of the available item, (3) label of the blocked item
   string            m_label_text[];
   string            m_path_bmp_on[];
   string            m_path_bmp_off[];
   //---
public:
   //--- Adds a menu item with specified properties before the creation of a context menu
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type);
   //---
  };
//+------------------------------------------------------------------+
//| Adds a menu item                                                 |
//+------------------------------------------------------------------+
void CContextMenu::AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type)
  {
//--- Increase the size of the arrays by one element
   int array_size=::ArraySize(m_items);
   ::ArrayResize(m_items,array_size+1);
   ::ArrayResize(m_label_text,array_size+1);
   ::ArrayResize(m_path_bmp_on,array_size+1);
   ::ArrayResize(m_path_bmp_off,array_size+1);
//--- Store the values of passed parameters
   m_label_text[array_size]   =text;
   m_path_bmp_on[array_size]  =path_bmp_on;
   m_path_bmp_off[array_size] =path_bmp_off;
//--- Setting the type of the menu item
   m_items[array_size].TypeMenuItem(type);
  }

To add a separation line in a context menu, we will need an array to store the index number of the menu item after which this line is to be set. The index number of the menu item will be passed to the CContextMenu::AddSeparateLine() method. The code is shown below.

class CContextMenu : public CElement
  {
private:
   //--- Array of index numbers of menu items after which a separation line is to be set
   int               m_sep_line_index[];
   //---
public:
   //--- Adds a separation line after the specified item before the creation of a context menu
   void              AddSeparateLine(const int item_index);
   //---
  };
//+------------------------------------------------------------------+
//| Adds a separation line                                           |
//+------------------------------------------------------------------+
void CContextMenu::AddSeparateLine(const int item_index)
  {
//--- Increase the size of the arrays by one element
   int array_size=::ArraySize(m_sep_line);
   ::ArrayResize(m_sep_line,array_size+1);
   ::ArrayResize(m_sep_line_index,array_size+1);
//--- Store the index number
   m_sep_line_index[array_size]=item_index;
  }

We will need methods which will allow us to get the following, having specified the index of the menu item: (1) the pointer to the menu item, (2) description (displayed text) and (3) the type. In every method, before returning the value of the property, at first a check for exceeding the array size is carried out and then the index is adjusted. It is implemented so that if the passed index is greater that the array size, then the last item will be called and if the index is less than zero, then the first item will be called. 

class CContextMenu : public CElement
  {
public:
   //--- Returns the item pointer from the context menu
   CMenuItem        *ItemPointerByIndex(const int index);
   //--- Returns description (displayed text)
   string            DescriptionByIndex(const int index);
   //--- Returns the menu item type
   ENUM_TYPE_MENU_ITEM TypeMenuItemByIndex(const int index);
   //---
  };
//+------------------------------------------------------------------+
//| Returns the menu item pointer by the index                       |
//+------------------------------------------------------------------+
CMenuItem *CContextMenu::ItemPointerByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- If there is no item in the context menu, report
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This method is to be called, "
              "if the context menu has at least one item!");
     }
//--- Adjustment in case the range has been exceeded
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Return the pointer
   return(::GetPointer(m_items[i]));
  }
//+------------------------------------------------------------------+
//| Returns the item name by the index                               |
//+------------------------------------------------------------------+
string CContextMenu::DescriptionByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- If there is no item in the context menu, report
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This method is to be called, "
              "if the context menu has at least one item!");
     }
//--- Adjustment in case the range has been exceeded
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Return the item description
   return(m_items[i].LabelText());
  }
//+------------------------------------------------------------------+
//| Returns the item type by the index                               |
//+------------------------------------------------------------------+
ENUM_TYPE_MENU_ITEM CContextMenu::TypeMenuItemByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- If there is no item in the context menu, report
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This method is to be called, "
              "if the context menu has at least one item!");
     }
//--- Adjustment in case the range has been exceeded
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Return the item type
   return(m_items[i].TypeMenuItem());
  }

One context menu may have several groups of radio items. To avoid confusion when identifying what item was clicked on, every radio item must have its own group identifier and the index in relation to the list of this group. The schematic below shows that besides general indices and the identifier of the context menu element, a radio item has its own distinguishing characteristics. 

Fig. 3. Schematic of identifiers and indices of different groups in a context menu.

Fig. 3. Schematic of identifiers and indices of different groups in a context menu.

 

When forming a context menu, the type of the menu item has to be identified before the menu is attached to the chart. In case this is a radio item, the group it belongs to has to be specified. In other words, we need a method that will allow us to determine the identifier of each radio item. The default identifiers of radio items will be equal to zero. If we leave them like this, a context menu will have only one group of radio items no matter how many we add. There will be situations when it is required to establish the identifier of the radio item and which of them is currently highlighted. There also must be a possibility to switch radio items. 

In addition to that, methods for working with checkboxes will be required. They will allow to find out the state of a checkbox and change its state if necessary. The declaration and implementation of these methods is in the code below.

class CContextMenu : public CElement
  {
public:
   //--- (1) Getting and (2) setting the checkbox state
   bool              CheckBoxStateByIndex(const int index);
   void              CheckBoxStateByIndex(const int index,const bool state);
   //--- (1) Returns and (2) sets the id of the radio item by the index
   int               RadioItemIdByIndex(const int index);
   void              RadioItemIdByIndex(const int item_index,const int radio_id);
   //--- (1) Returns selected radio item, (2) switches the radio item
   int               SelectedRadioItem(const int radio_id);
   void              SelectedRadioItem(const int radio_index,const int radio_id);
   //---
  };
//+------------------------------------------------------------------+
//| Returns the checkbox state by the index                          |
//+------------------------------------------------------------------+
bool CContextMenu::CheckBoxStateByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- If there is no item in the context menu, report
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This method is to be called, "
              "if the context menu has at least one item!");
     }
//--- Adjustment in case the range has been exceeded
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Return the item state
   return(m_items[i].CheckBoxState());
  }
//+------------------------------------------------------------------+
//| Sets the checkbox state by the index                             |
//+------------------------------------------------------------------+
void CContextMenu::CheckBoxStateByIndex(const int index,const bool state)
  {
//--- Check for exceeding the range
   int size=::ArraySize(m_items);
   if(size<1 || index<0 || index>=size)
      return;
//--- Set the state
   m_items[index].CheckBoxState(state);
  }
//+------------------------------------------------------------------+
//| Returns the radio item id by the index                           |
//+------------------------------------------------------------------+
int CContextMenu::RadioItemIdByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- If there is no item in the context menu, report
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > This method is to be called, "
              "if the context menu has at least one item!");
     }
//--- Adjustment in case the range has been exceeded
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Return the identifier
   return(m_items[i].RadioButtonID());
  }
//+------------------------------------------------------------------+
//| Sets the radio item id by the index                              |
//+------------------------------------------------------------------+
void CContextMenu::RadioItemIdByIndex(const int index,const int id)
  {
//--- Check for exceeding the range
   int array_size=::ArraySize(m_items);
   if(array_size<1 || index<0 || index>=array_size)
      return;
//--- Set the identifier
   m_items[index].RadioButtonID(id);
  }
//+------------------------------------------------------------------+
//| Returns the radio item index by the id                           |
//+------------------------------------------------------------------+
int CContextMenu::SelectedRadioItem(const int radio_id)
  {
//--- Radio item counter
   int count_radio_id=0;
//--- Iterate over the list of context menu items
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Move to the following if this is not a radio item
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- If identifiers match
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- If this is an active radio item, leave the loop
         if(m_items[i].RadioButtonState())
            break;
         //--- Increase the counter of radio items
         count_radio_id++;
        }
     }
//--- Return the index
   return(count_radio_id);
  }
//+------------------------------------------------------------------+
//| Switches the radio item by the index and id                      |
//+------------------------------------------------------------------+
void CContextMenu::SelectedRadioItem(const int radio_index,const int radio_id)
  {
//--- Radio item counter
   int count_radio_id=0;
//--- Iterate over the list of context menu items
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Move to the following if this is not a radio item
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- If identifiers match
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Switch the radio item
         if(count_radio_id==radio_index)
            m_items[i].RadioButtonState(true);
         else
            m_items[i].RadioButtonState(false);
         //--- Increase the counter of radio items
         count_radio_id++;
        }
     }
  }

Everything is ready for creating methods for attaching a context menu to the chart. The attachment will have three stages:

Every stage requires a private method. Later, they will be called in the common public method. Declare them in the class body:

class CContextMenu : public CElement
  {
public:
   //--- Methods for creating a context menu
   bool              CreateContextMenu(const long chart_id,const int window,const int x=0,const int y=0);
   //---
private:
   bool              CreateArea(void);
   bool              CreateItems(void);
   bool              CreateSeparateLine(const int line_number,const int x,const int y);
   //---
  };

The height of the context menu background depends on the number of items and separation lines. That is why there is no point in setting this value in the application class as this value will be replaced in the CContextMenu::CreateArea() method which is designed for setting the context menu background. The height for the separation line area will be equal to nine pixels and, therefore, the number of lines must be multiplied by this value to calculate the area they occupy.

//+------------------------------------------------------------------+
//| Creates the common area of the context menu                      |
//+------------------------------------------------------------------+
bool CContextMenu::CreateArea(void)
  {
//--- Forming the object name
   string name=CElement::ProgramName()+"_contextmenu_bg_"+(string)CElement::Id();
//--- Calculation of the context menu height depends on the number of menu items and separation lines
   int items_total =ItemsTotal();
   int sep_y_size  =::ArraySize(m_sep_line)*9;
   m_y_size        =(m_item_y_size*items_total+2)+sep_y_size-(items_total-1);
//--- Set up the context menu background
   if(!m_area.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,m_y_size))
      return(false);
//--- Set properties
   m_area.BackColor(m_area_color);
   m_area.Color(m_area_border_color);
   m_area.BorderType(BORDER_FLAT);
   m_area.Corner(m_corner);
   m_area.Selectable(false);
   m_area.Z_Order(m_area_zorder);
   m_area.Tooltip("\n");
//--- Margins from the edge point
   m_area.XGap(m_x-m_wnd.X());
   m_area.YGap(m_y-m_wnd.Y());
//--- Размеры фона
   m_area.XSize(m_x_size);
   m_area.YSize(m_y_size);
//--- Set the object pointer
   CElement::AddToArray(m_area);
   return(true);
  }

Separation lines will be set by the CContextMenu::CreateSeparateLine() method. The line number and coordinates are to be passed to this method as parameters:

//+------------------------------------------------------------------+
//| Creates a separation line                                        |
//+------------------------------------------------------------------+
bool CContextMenu::CreateSeparateLine(const int line_number,const int x,const int y)
  {
//--- Store the form pointer
   m_sep_line[line_number].WindowPointer(m_wnd);
//--- Set properties
   m_sep_line[line_number].TypeSepLine(H_SEP_LINE);
   m_sep_line[line_number].DarkColor(m_sepline_dark_color);
   m_sep_line[line_number].LightColor(m_sepline_light_color);
//--- Creating a separation line
   if(!m_sep_line[line_number].CreateSeparateLine(m_chart_id,m_subwin,line_number,x,y,m_x_size-10,2))
      return(false);
//--- Set the object pointer
   CElement::AddToArray(m_sep_line[line_number].Object(0));
   return(true);
  }

The CContextMenu::CreateSeparateLine() method will be called in the CContextMenu::CreateItems() method for setting menu items. Coordinates and the sequence of setting these element will be defined in one loop. Earlier, we have considered the m_sep_line_index[] array. The index numbers of the menu items after which a separation line is to be set will be stored in this array when a context menu is formed. Comparing the number of the current iteration of the loop and index numbers of the menu items in the m_sep_line_index[] array can identify after which point the separation line is to be set. 

It is also necessary to store the pointer to the previous node before setting each item in the context menu. The code of the CContextMenu::CreateItems() method with detailed comments is presented below.

//+------------------------------------------------------------------+
//| Creates a list of menu items                                     |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // For identification of the location of separation lines
   int x =m_x+1; // X coordinate
   int y =m_y+1; // Y coordinate. Will be calculated in a loop for each menu item.
//--- Number of separation lines
   int sep_lines_total=::ArraySize(m_sep_line_index);
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Calculation of the Y coordinate
      y=(i>0)? y+m_item_y_size-1 : y;
      //--- Store the form pointer
      m_items[i].WindowPointer(m_wnd);
      //--- Add the pointer to the previous node
      m_items[i].PrevNodePointer(m_prev_node);
      //--- Set properties
      m_items[i].XSize(m_x_size-2);
      m_items[i].YSize(m_item_y_size);
      m_items[i].IconFileOn(m_path_bmp_on[i]);
      m_items[i].IconFileOff(m_path_bmp_off[i]);
      m_items[i].AreaBackColor(m_area_color);
      m_items[i].AreaBackColorOff(m_item_back_color_hover_off);
      m_items[i].AreaBorderColor(m_area_color);
      m_items[i].LabelColor(m_label_color);
      m_items[i].LabelColorHover(m_label_color_hover);
      m_items[i].RightArrowFileOn(m_right_arrow_file_on);
      m_items[i].RightArrowFileOff(m_right_arrow_file_off);
      m_items[i].IsDropdown(m_is_dropdown);
      //--- Margins from the edge point of the panel
      m_items[i].XGap(x-m_wnd.X());
      m_items[i].YGap(y-m_wnd.Y());
      //--- Creating a menu item
      if(!m_items[i].CreateMenuItem(m_chart_id,m_subwin,i,m_label_text[i],x,y))
         return(false);
      //--- Move to the next one if all separation lines are set up
      if(s>=sep_lines_total)
         continue;
      //--- If all indices match, then a separation line can be set up after this item
      if(i==m_sep_line_index[s])
        {
         //--- Coordinates
         int l_x=x+5;
         y=y+m_item_y_size+2;
         //--- Setting up a separation line
         if(!CreateSeparateLine(s,l_x,y))
            return(false);
         //--- Adjustment of the Y coordinate for the following item
         y=y-m_item_y_size+7;
         //--- Increase the counter for separation lines
         s++;
        }
     }
   return(true);
  }

Then, the CContextMenu::CreateContextMenu() method for external use has to be implemented. At this stage, let us consider an option where a context menu must be attached to an item of the external menu or to an independent lone menu item. That means that before creating a context menu, it has to be passed a pointer to the previous node as was said earlier. Besides, the check for the presence of the form pointer, a check for the presence of the pointer to the previous node must be carried out. For the library user, this will be an additional reference point eliminating a possibility of incorrect graphical interface formation. 

A context menu is usually hidden after creation as it is designed to be brought up by either clicking on another control or clicking on the working area. The Hide() method is designed for hiding objects in each element. There is a similar method in the CContextMenu class. At first objects of a context menu - background and a separation line are hidden by it. Then all menu items are hidden in a loop. At the same time, their own CMenuItem::Hide() methods are called for menu items. Separation lines could also be hidden in a similar way because this element has its own CSeparateLine::Hide() method. However, as this is only a design element, consisting of only one graphical object and is not designed for the interactions with the user, it was added to the common array of the context menu objects at the moment of creation and will be hidden in a corresponding loop.

//+------------------------------------------------------------------+
//| Hides the context menu                                           |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Leave, if the element is hidden
   if(!CElement::m_is_visible)
      return;
//--- Hide the objects of the context menu
   for(int i=0; i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_NO_PERIODS);
//--- Hide menu items
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Hide();
//--- Zero the focus
   CElement::MouseFocus(false);
//--- Assign the status of a hidden control
   CElement::m_is_visible=false;
  }

All the methods for managing a context menu will be structured in a similar way and we will not consider their code here. You can see the code in the files attached to this article. We will only discuss the code of the CContextMenu::Delete() method for removing the element. Here, besides removing all graphical objects, all arrays that were used for the formation of a context menu are emptied. If this is not done, then every time when a symbol or a timeframe is changed, the list of menu items will be increased. At the stage of testing, you can try to experiment with this by just commenting on those lines.

//+------------------------------------------------------------------+
//| Deletion                                                         |
//+------------------------------------------------------------------+
void CContextMenu::Delete(void)
  {
//--- Removing objects  
   m_area.Delete();
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Delete();
//--- Removing separation lines
   int sep_total=::ArraySize(m_sep_line);
   for(int i=0; i<sep_total; i++)
      m_sep_line[i].Delete();
//--- Emptying the control arrays
   ::ArrayFree(m_items);
   ::ArrayFree(m_sep_line);
   ::ArrayFree(m_sep_line_index);
   ::ArrayFree(m_label_text);
   ::ArrayFree(m_path_bmp_on);
   ::ArrayFree(m_path_bmp_off);
//--- Emptying the array of the objects
   CElement::FreeObjectsArray();
  }

Getting back to the method for creating a context menu, it must be mentioned that coordinates will be set in relation to the previous node. Let us arrange so that the library user can set custom coordinates if the necessity arises. Default coordinates will be set to zero in the CContextMenu::CreateContextMenu() method for creating a context menu. Coordinates will be calculated from the previous node automatically only unless at least one coordinate is specified. If both coordinates are specified, then an automatic calculation will be canceled.

For those context menus which are open from other context menus, their coordinates will be automatically calculated from the right part of the item to which the the menu is attached. For those context menus attached to the menu items of the main menu, coordinate calculation will be carried out from the lower part of the items. To manage this system, one more field and method for the CContextMenu class are required. Let us add a new enumeration to the Enums.mqh file:

//+------------------------------------------------------------------+
//| Enumeration of the menu attachment sides                            |
//+------------------------------------------------------------------+
enum ENUM_FIX_CONTEXT_MENU
  {
   FIX_RIGHT  =0,
   FIX_BOTTOM =1
  };

A corresponding field and the method for setting the coordinate calculation mode must be added to the class of the context menu. By default, the coordinates will be calculated from the right part of the item.

class CContextMenu : public CElement
  {
private:
   //--- Attachment side of the context menu
   ENUM_FIX_CONTEXT_MENU m_fix_side;
   //---
public:
   //--- Setting the context menu attachment mode
   void              FixSide(const ENUM_FIX_CONTEXT_MENU side)      { m_fix_side=side;                      }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_fix_side(FIX_RIGHT)
  {
  }

Below is the code of the CContextMenu::CreateContextMenu() method. Creating an element is possible only if there is a pointer to it. Properties of this node will be available only after a corresponding check discussed earlier has been completed and this will allow to calculate relative coordinates automatically. Hiding a context menu in the code must be located after its creation.

//+------------------------------------------------------------------+
//| Creates a context menu                                           |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Leave, if there is no form pointer
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Before creating a context menu it has to be passed  "
              "a window object using the WindowPointer(CWindow &object).");
      return(false);
     }
//--- Leave, if there is no pointer to the previous node 
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Before creating a context menu it has to be passed "
              "the pointer to the previous node using the CContextMenu::PrevNodePointer(CMenuItem &object) method.");
      return(false);
     }
//--- Initialization of variables
   m_id       =m_wnd.LastId()+1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
//--- If coordinates are not specified
   if(x==0 || y==0)
     {
      m_x =(m_fix_side==FIX_RIGHT)? m_prev_node.X2()-3 : m_prev_node.X()+1;
      m_y =(m_fix_side==FIX_RIGHT)? m_prev_node.Y()-1  : m_prev_node.Y2()-1;
     }
//--- If coordinates are specified
   else
     {
      m_x =x;
      m_y =y;
     } 
//--- Margins from the edge point
   CElement::XGap(m_x-m_wnd.X());
   CElement::YGap(m_y-m_wnd.Y());
//--- Creating the context menu
   if(!CreateArea())
      return(false);
   if(!CreateItems())
      return(false);
//--- Hide element
   Hide();
   return(true);
  }

A check for presence of the previous node pointer with a condition has to be added to the CMenuItem class of the CreateMenuItem() method. Absence of a pointer will mean that an independent menu item is implied. This means that this item is not a part of a context menu. Such items can be either items of a simple type (MI_SIMPLE) or items containing a context menu (MI_HAS_CONTEXT_MENU). It might be difficult to understand this right now but it will become clear when we look at examples at the end of this article.

Place this code to the CMenuItem::CreateMenuItem() method after checking for presence of the form pointer as shown below.

//--- If there is no pointer to the previous node, then
//    an independent menu item is implied, that is the one that is not a part of the context menu
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      //--- Leave, if the set type does not match
      if(m_type_menu_item!=MI_SIMPLE && m_type_menu_item!=MI_HAS_CONTEXT_MENU)
        {
         ::Print(__FUNCTION__," > The type of the independent menu item can be only MI_SIMPLE or MI_HAS_CONTEXT_MENU,",
                 "that is only with a context menu.\n",
                 __FUNCTION__," > The type of the menu item can be set using the CMenuItem::TypeMenuItem() method");
         return(false);
        }
     }



Test of Attaching a Context Menu

Attaching a context menu to the chart can be tested right now. Include the ContextMenu.mqh file with the CContextMenu class of the menu to the library as shown below.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Window.mqh"
#include "MenuItem.mqh"
#include "ContextMenu.mqh"
#include "SeparateLine.mqh"

Create an instance of the CContextMenu class in the custom class of the CProgram application and declare the method for creating a context menu. Margins from the edge point of the form do not have to be specified as they will be calculated in relation to the menu item to which they are attached.

class CProgram : public CWndEvents
  {
private:
   //--- Menu item and context menu
   CMenuItem         m_menu_item1;
   CContextMenu      m_mi1_contextmenu1;
   //---
private:
#define MENU_ITEM1_GAP_X (6)
#define MENU_ITEM1_GAP_Y (25)
   bool              CreateMenuItem1(const string item_text);
   bool              CreateMI1ContextMenu1(void);
  };

Now, let us form the context menu. It will have five items: three simple ones (MI_SIMPLE) and two items of the checkbox type (MI_CHECKBOX). Include the resources with icons for their labels for simple items outside of the method body. The icon will be colored for the available item and colorless for the blocked one. You can download them by the links at the end of this article. Then, at the very beginning of the method, store the pointers to the form and the previous node in the context menu. Without these actions, the graphical interface will not be created and the program will be removed from the chart. Then follow the arrays with (1) the item description (displayed text), with the icons of (2) the available and (3) blocked states (4) and item types. After that, we need to specify common properties of all items and then add them to the class of the context menu in a loop using the CContextMenu::AddItem() method. Add a separation line after the second item (index 1). After all these actions have been completed, the context menu can be attached to the chart. At the very end of the method, add the element pointer to the base. Below is the code of the method.

//+------------------------------------------------------------------+
//| Creates a context menu                                           |
//+------------------------------------------------------------------+
#resource "\\Images\\Controls\\coins.bmp"
#resource "\\Images\\Controls\\coins_colorless.bmp"
#resource "\\Images\\Controls\\line_chart.bmp"
#resource "\\Images\\Controls\\line_chart_colorless.bmp"
#resource "\\Images\\Controls\\safe.bmp"
#resource "\\Images\\Controls\\safe_colorless.bmp"
//---
bool CProgram::CreateMI1ContextMenu1(void)
  {
//--- Five items in the context menu
#define CONTEXTMENU_ITEMS1 5
//--- Store the window pointer
   m_mi1_contextmenu1.WindowPointer(m_window);
//--- Store the pointer to the previous node
   m_mi1_contextmenu1.PrevNodePointer(m_menu_item1);
//--- Array of item names
   string items_text[CONTEXTMENU_ITEMS1]=
     {
      "ContextMenu 1 Item 1",
      "ContextMenu 1 Item 2",
      "ContextMenu 1 Item 3",
      "ContextMenu 1 Item 4",
      "ContextMenu 1 Item 5"
     };
//--- Label array for the available mode
   string items_bmp_on[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins.bmp",
      "Images\\Controls\\line_chart.bmp",
      "Images\\Controls\\safe.bmp",
      "",""
     };
//--- Label array for the blocked mode
   string items_bmp_off[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins_colorless.bmp",
      "Images\\Controls\\line_chart_colorless.bmp",
      "Images\\Controls\\safe_colorless.bmp",
      "",""
     };
//--- Array of item types
   ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS1]=
     {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_SIMPLE,
      MI_CHECKBOX,
      MI_CHECKBOX
     };
//--- Set up properties before creation
   m_mi1_contextmenu1.XSize(160);
   m_mi1_contextmenu1.ItemYSize(24);
   m_mi1_contextmenu1.AreaBackColor(C'240,240,240');
   m_mi1_contextmenu1.AreaBorderColor(clrSilver);
   m_mi1_contextmenu1.ItemBackColorHover(C'240,240,240');
   m_mi1_contextmenu1.ItemBackColorHoverOff(clrLightGray);
   m_mi1_contextmenu1.ItemBorderColor(C'240,240,240');
   m_mi1_contextmenu1.LabelColor(clrBlack);
   m_mi1_contextmenu1.LabelColorHover(clrWhite);
   m_mi1_contextmenu1.RightArrowFileOff("Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp");
   m_mi1_contextmenu1.SeparateLineDarkColor(C'160,160,160');
   m_mi1_contextmenu1.SeparateLineLightColor(clrWhite);
//--- Add items to the context menu
   for(int i=0; i<CONTEXTMENU_ITEMS1; i++)
      m_mi1_contextmenu1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i],items_type[i]);
//--- Separation line after the second item
   m_mi1_contextmenu1.AddSeparateLine(1);
//--- Create the context menu
   if(!m_mi1_contextmenu1.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Add the control pointer to the base
   CWndContainer::AddToElementsArray(0,m_mi1_contextmenu1);
   return(true);
  }

Now, add the call of the method for creating a context menu to the main method for creating the graphical interface. Although the context menu is to be hidden at the set up, it will be shown in this test. The code below shows what lines have to be added to the CProgram::CreateTradePanel() method.

//+------------------------------------------------------------------+
//| Creates a trading panel                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creating a form for controls
   if(!CreateWindow("EXPERT PANEL"))
      return(false);
//--- Creating controls:
//    Menu item
   if(!CreateMenuItem1("Menu item"))
      return(false);
   if(!CreateMI1ContextMenu1())
      return(false);
//--- Separation line
   if(!CreateSepLine())
      return(false);
//--- Show the context menu
   m_mi1_contextmenu1.Show();
//--- Redrawing of the chart
   m_chart.Redraw();
   return(true);
  }

Compile the files and load the program on to the chart. The result should be as in the screenshot below

Fig. 4. Test of the context menu element.

Fig. 4. Test of the context menu element.


At this stage, when the mouse cursor is hovering over the items of the context menu, they will not change their color. This functionality can be created in the CContextMenu class of the context menu or you can use a ready-made one in the CMenuItem class of the menu item. After the context menu has been attached to the chart, its pointer is added to the base. However, the pointers to each menu item stay unavailable for use in the CWndEvents class for event handling in the current implementation of adding pointers to the common array for elements. For each complex (compound) control, which consists of several elements, we will create in the CWndContainer class a method in which the pointers to these elements can be obtained. For that, we implemented the ItemPointerByIndex() method in the CContextMenu class, using which we can get the pointer to the menu item by specifying its index.

 


Further Development of the Class for Storing the Pointers to All Elements

Let us implement the AddContextMenuElements() method in the CWndContainer class for work with the context menu element. The form index and the element object are to be passed to it as parameters. At the beginning of the method, a check will be carried out if the passed element is a context menu. If it is, then a pointer to the context menu (CContextMenu) is required to get access to its methods. How can this be done if the object that is being passed belongs to the base class (CElement)? For that, it is sufficient to declare a pointer with the CContextMenu type and assign the pointer to the passed object to it. In the code below it is highlighted in yellow. This way, there is an access to the items of the context menu. Then, they are added in a loop to the common array of elements of their form. At the end of each iteration, menu items are passed to the CWndContainer::AddToObjectsArray() method for storing pointers to the objects constituting them in the object array of the CChartObject type.

//+------------------------------------------------------------------+
//| Class for storing all interface objects                          |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Stores pointers to the context menu elements in the base
   bool              AddContextMenuElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Stores pointers to the context menu elements in the base         |
//+------------------------------------------------------------------+
bool CWndContainer::AddContextMenuElements(const int window_index,CElement &object)
  {
//--- Leave, if this is not a context menu
   if(object.ClassName()!="CContextMenu")
      return(false);
//--- Get the context menu pointers
   CContextMenu *cm=::GetPointer(object);
//--- Store the pointers to its objects in the base
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Increasing the element array
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Getting the menu item pointers
      CMenuItem *mi=cm.ItemPointerByIndex(i);
      //--- Store the pointer in the array
      m_wnd[window_index].m_elements[size]=mi;
      //--- Add pointers to all the objects of a menu item to the common array
      AddToObjectsArray(window_index,mi);
     }
//---
   return(true);
  }

This will be called in the CWndContainer::AddToElementsArray() method straight after increasing the element counter. For the sake of saving space in the article, we show only a shortened version of the code. The full version can be found in the files attached to this article.

//+------------------------------------------------------------------+
//| Adds a pointer to the element array                              |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- If the base does not contain forms for controls
//--- If the request is for a non-existent form
//--- Add to the common array of elements
//--- Add element objects to the common array of objects
//--- Store the id of the last element in all forms
//--- Increase the counter of element identifiers

//--- Stores the pointers to the context menu objects in the base
   if(AddContextMenuElements(window_index,object))
      return;
  }

The code of the CWndContainer::AddToElementsArray() method will be enriched in the same way with other similar methods for complex (compound) elements.

If we compile all the files and load the program on to the chart, the items of the context menu will change their appearance when the mouse cursor is hovering over them.

Fig. 5. Test of the context menu items.

Fig. 5. Test of the context menu items.


Development of the class for creating a context menu is completed. The next step is to set up its event handler and the event handler of menu items. We will deal with this in the next article.

 


Conclusion

Our library already contains three classes for creating such elements as:

In the next article, we will set up event handlers of the library in the main class and in the classes of controls created earlier.

You can find and download archives with the library files at the current stage of development, icons and files of the programs (the EA, indicators and the script) considered in this article for testing in the Metatrader 4 and Metatrader 5 terminals. 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 second part: