Graphical Interfaces II: Setting Up the Event Handlers of the Library (Chapter 3)

Anatoli Kazharski | 21 March, 2016

Contents

 

 

Introduction

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

The previous articles contain the implementation of the classes for creating constituent parts of the main menu. The development of the class of each control requires prior fine adjustment of the event handlers in the principle base classes and in the classes of created controls. The following questions will be considered in this article:

Added to that, the process of receiving messages in the handler of the custom class of the application will be shown. 

 


Private Arrays of the Elements

Let us conduct a little experiment. Left click on one of the context menu items in the area where the mouse cursor will be outside of the form area. We will see that the chart scroll has not been disabled and it can be used when hovering over the control. This is a functional error and it should not be there. We are going to arrange so that no matter what control the mouse cursor is over, the chart scroll and the moving of trading levels mode are disabled at that time. 

First of all, let us add tracking the focus on the element to the context menu handler as shown in the code below. If the context menu is hidden, then there is no need to continue. Follow this approach to save time.

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      if(!CElement::m_is_visible)
         return;
      //--- Get the focus
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
     }
  }

At the current stage of the library development, the element base or the CWndContainer class to be precise, contains the m_elements[] common array of element pointers. This is a part of the WindowElements structure of the element arrays. This array is suitable for all the cases when an action has to be applied to all controls or majority of them. If an action has to be applied only to a certain group of elements, this approach is excessive as it requires too many resources. For instance, let us consider a group of controls with sizes that can exceed the boundaries of the form they are attached to. Drop-down lists and context menus belong to this group. Every type of such elements must be stored in separate arrays. This will allow more efficient and easy management. 

Add the array for context menus to the WindowElements structure and create a method for getting its size:

//+------------------------------------------------------------------+
//| Class for storing all interface objects                          |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Structure of the elements arrays
   struct WindowElements
     {
      //--- Common array of all objects
      CChartObject     *m_objects[];
      //--- Common array of all elements
      CElement         *m_elements[];
      
      //--- Private arrays of elements:
      //    Context menu array
      CContextMenu     *m_context_menus[];
     };
   //--- Array of element arrays for each window
   WindowElements    m_wnd[];
   //---
public:
   //--- Number of context menus
   int               ContextMenusTotal(const int window_index);
   //---
  };
//+------------------------------------------------------------------+
//| Returns the number of context menus by the specified window index|
//+------------------------------------------------------------------+
int CWndContainer::ContextMenusTotal(const int window_index)
  {
   if(window_index>=::ArraySize(m_wnd))
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return(::ArraySize(m_wnd[window_index].m_context_menus));
  }

Every time after creating a control in the custom class of the application (CProgram in our case), we use the CWndContainer::AddToElementsArray() method to add a pointer to this control to the base. In this method, methods for getting and storing pointers to every complex (compound) element in the common array will be used. A similar method CWndContainer::AddContextMenuElements() was created earlier for the context menu. All similar methods provide a possibility of distributing pointers in the private arrays of the element if there is the necessity.

Then, we require a template method for adding an element pointer to the array passed by a link because this action will be repeated more than once and applied to different object types.

class CWndContainer
  {
protected:
   //--- Template method for adding pointers to the array passed by a link
   template<typename T1,typename T2>
   void              AddToRefArray(T1 &object,T2 &ref_array[]);
   //---
  };
//+------------------------------------------------------------------+
//| Stores the pointer (T1) in the array passed by a link (T2)       |
//+------------------------------------------------------------------+
template<typename T1,typename T2>
void CWndContainer::AddToRefArray(T1 &object,T2 &array[])
  {
   int size=::ArraySize(array);
   ::ArrayResize(array,size+1);
   array[size]=object;
  }

Now, the context menu pointer can be stored in its private array at the end of the CWndContainer::AddContextMenuElements()method as show below (highlighted in yellow). Let us do the same for all other controls.

//+------------------------------------------------------------------+
//| Stores the pointers to the context menu objects 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 pointer
   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 pointer
      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);
     }
//--- Add the pointer to the private array
   AddToRefArray(cm,m_wnd[window_index].m_context_menus);
   return(true);
  }

 


Managing the State of the Chart

Then, a method for checking the focus of the mouse cursor over controls has to be added in the CWndEvents class. Such a check will be conducted for forms and drop-down lists. Both forms and context menus already have private arrays. Therefore, let us create the CWndEvents::SetChartState() method. Below is the declaration and implementation of this method:

class CWndEvents : public CWndContainer
  {
private:
   //--- Setting the chart state
   void              SetChartState(void);
  };
//+------------------------------------------------------------------+
//| Sets the chart state                                             |
//+------------------------------------------------------------------+
void CWndEvents::SetChartState(void)
  {
//--- For identifying the event when management has to be disabled
   bool condition=false;
//--- Check windows
   int windows_total=CWndContainer::WindowsTotal();
   for(int i=0; i<windows_total; i++)
     {
      //--- Move to the following form if this one is hidden

      if(!m_windows[i].IsVisible())
         continue;
      //--- Check conditions in the internal handler of the form
      m_windows[i].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
      //--- If there is a focus, register this
      if(m_windows[i].MouseFocus())
        {
         condition=true;
         break;
        }
     }
//--- Check the focus of the context menus
   if(!condition)
     {
      int context_menus_total=CWndContainer::ContextMenusTotal(0);
      for(int i=0; i<context_menus_total; i++)
        {
         if(m_wnd[0].m_context_menus[i].MouseFocus())
           {
            condition=true;
            break;
           }
        }
     }
//---
   if(condition)
     {
      //--- Disable scroll and management of trading levels
      m_chart.MouseScroll(false);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,false);
     }
   else
     {
      //--- Enable management
      m_chart.MouseScroll(true);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,true);
     }
  }

This method will be enriched with some additions later but it is already suitable for the current task. It must be called in the CWndEvents::ChartEventMouseMove() method as shown below.

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

Compile all the files and test the EA. We can see now that when left clicking in the context menu area, which exceeds the boundaries of the form, the chart scrolling and management of the trading levels are disabled. The test of attaching an element to the chart was successful. From now on, a context menu will be brought up only by the user request. Remove its display from the CProgram::CreateTradePanel() method in the application class (see the code below).

   m_contextmenu.Show(); // <<< This line of code must be removed

 


Identifiers for External and Internal Use

Now, we are going to proceed to handling of the left clicking on the menu item.

Our next task is to arrange bringing up a context menu by clicking on the menu item, provided that the context menu is enclosed. The second click must hide it. Such handling will be present both in the CMenuItem class of the menu item and in the CContextMenu class of the context menu. The thing is that the context menu has access to the item it is attached to (the previous node) and the menu item that contains the context menu does not have a direct access to it. The context menu pointer cannot be created in the CMenuItem class. This is because if the ContextMenu.mqh file is included to the MenuItem.mqh file, there will be compilation errors. That is why we will carry out handling of the context menu display in the CContextMenu class. The handler in the CMenuItem class will be auxiliary. It will be generating a custom event by sending specific information to the context menu about the menu item which was clicked on. Besides, we need to make the context menu hide when a click is made outside of the context menu area, like it is done in the MetaTrader terminals and the MetaEditor code editor. This is a standard behavior for context menus. 

To implement this functionality, additional identifiers for custom events are required. Some of them will be designed for internal use in the library classes and some of them for external handling in the custom application class. In our case this is CProgram

Events for internal use:

For external use, create the ON_CLICK_CONTEXTMENU_ITEM identifier that will inform the program that the click happened on the item of the context menu.

Place listed identifiers with unique numbers assigned to each of them in the Defines.mqh file:

#define ON_CLICK_MENU_ITEM        (4) // Clicking on the menu item
#define ON_CLICK_CONTEXTMENU_ITEM (5) // Clicking on the menu item of the context menu
#define ON_HIDE_CONTEXTMENUS      (6) // Hide all context menus
#define ON_HIDE_BACK_CONTEXTMENUS (7) // Hide the context menus below the current menu item

 


Enriching the Class of the Context Menu

The following fields and methods must be added in the CContextMenu class of the context menu: 

The code below presents the declaration and implementation of everything listed above with detailed comments:

class CContextMenu : public CElement
  {
private:
   //--- State of the context menu
   bool              m_contextmenu_state;
public:   
   //--- (1) Getting and (2) setting the context menu state
   bool              ContextMenuState(void)                   const { return(m_context_menu_state);         }
   void              ContextMenuState(const bool flag)              { m_context_menu_state=flag;            }
   //---
private:
   //--- Handling clicking on the item to which this context menu is attached
   bool              OnClickMenuItem(const string clicked_object);
   //--- Getting (1) the identifier and (2) index from the menu item name
   int               IdFromObjectName(const string object_name);
   int               IndexFromObjectName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Handling clicking on the menu item                               |
//+------------------------------------------------------------------+
bool CContextMenu::OnClickMenuItem(const string clicked_object)
  {
//--- Leave, if the context menu is already open 
   if(m_contextmenu_state)
      return(true);
//--- Leave, if the clicking was not on the menu item
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0)
      return(false);
//--- Get the identifier and the index from the object name
   int id    =IdFromObjectName(clicked_object);
   int index =IndexFromObjectName(clicked_object);
//--- Leave, if the clicking was not on the menu item to which this context menu is attached
   if(id!=m_prev_node.Id() || index!=m_prev_node.Index())
      return(false);
//--- Show the context menu
   Show();
   return(true);
  }
//+------------------------------------------------------------------+
//| Extracts the identifier from the object name                     |
//+------------------------------------------------------------------+
int CContextMenu::IdFromObjectName(const string object_name)
  {
//--- Get the id from the object name
   int    length =::StringLen(object_name);
   int    pos    =::StringFind(object_name,"__",0);
   string id     =::StringSubstr(object_name,pos+2,length-1);
//---
   return((int)id);
  }
//+------------------------------------------------------------------+
//| Extracts the index from the object name                          |
//+------------------------------------------------------------------+
int CContextMenu::IndexFromObjectName(const string object_name)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Get the code of the separator
   u_sep=::StringGetCharacter("_",0);
//--- Split the string
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-1;
//--- Checking for exceeding the array range
   if(array_size-2<0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return((int)result[array_size-2]);
  }

Now, we only need to add the call of the CContextMenu::OnClickMenuItem() method when the CHARTEVENT_OBJECT_CLICK() event takes place to the CContextMenu::OnEvent event handler of the context menu:

//--- Handling left mouse clicking event on an object
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickMenuItem(sparam))
         return;
     }

 


Enriching the Class of the Menu Item

When the program detects left mouse click on a menu item, it will pass a string parameter to the CContextMenu::OnClickMenuItem() method. The string parameter contains the name of the rectangle label graphical object, which is the background of the menu item. As you remember, the priority for the click on the background will be higher than for the click on other element objects for almost all controls. This guarantees that the click will not be intercepted by any other element object, which can trigger unexpected behavior of the program. For instance, if the label of the menu item has a higher priority than its background, then clicking on the label area can lead to changing of the icon. Let me remind you that we have label icons defined for two states. The reason for that is that all objects of the OBJ_BITMAP_LABEL type will behave this way by default. 

At the beginning of the CContextMenu::OnClickMenuItem() method, a check of the context menu state will be conducted. If it is already enabled, then there is no need to carry on. Then, the name of the object that was clicked on is checked. If this is an object of our program and there is an indication that this is a menu item, then we carry on. The identifier and index of the menu item are extracted from the object name. For those tasks we already have designated methods in which all required parameters are extracted from the object name using string functions of the MQL language. The menu item identifier is extracted using the double dash string as a delimiter. To extract an index, the line is split into parts by the underscore symbol (_), which is a separator of the element object parameters.

Create the OnClickMenuItem() method in the CMenuItem class. Its code will differ from the one written for the context menu. Below are the declaration and implementation of this method. In this method there is no necessity to extract parameters from the object name. It is sufficient to compare the background name with the name of the passed object. Then, the current state of the menu item is checked. If it is blocked, then further actions are not required. After that, if the item contains a context menu, the status of enabled or disabled element is assigned to it. If before that the state of the context menu was enabled, then the main module of the event handling sends a signal for closing all context menus that were open later. This is applicable for those cases when several context menus which open from one another are open simultaneously. Such examples will be discussed further in the article. Besides the ON_HIDE_BACK_CONTEXTMENUS event identifier, the menu item identifier is passed as another parameter. This is used to identify on which context menu the loop can be stopped.

class CMenuItem : public CElement
  {
   //--- Handling clicking on the menu item
   bool              OnClickMenuItem(const string clicked_object);
   //---
  };
//+------------------------------------------------------------------+
//| Handling clicking on the menu item                               |
//+------------------------------------------------------------------+
bool CMenuItem::OnClickMenuItem(const string clicked_object)
  {
//--- Check by the object name
   if(m_area.Name()!=clicked_object)
      return(false);
//--- Leave, if the item has not been activated
   if(!m_item_state)
      return(false);
//--- If this item contains a context menu
   if(m_type_menu_item==MI_HAS_CONTEXT_MENU)
     {
      //--- If the drop-down menu of this item has not been activated
      if(!m_context_menu_state)
        {
         m_context_menu_state=true;
        }
      else
        {
         m_context_menu_state=false;
         //--- Send a signal for closing context menus, which are below this item
         ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,"");
        }
      return(true);
     }
//--- If this item does not contain a context menu, but is a part of a context menu itself
   else
     {
     }
//---
   return(true);
  }

 

 


Enriching the Main Class of Handling Events of the Graphical Interface

This is not the final version of the CMenuItem::OnClickMenuItem() method, and we will get back to it later to introduce some additions. Currently, its main task is to send a message for hiding the context menu to the principle module of handling custom events in the CWndEvents class. In that class, let us create a method access to which will be carried out by the ON_HIDE_BACK_CONTEXTMENUS event. Let us name it CWndEvents::OnHideBackContextMenus(). The code of this method is presented below:

class CWndEvents : public CWndContainer
  {
private:
   //--- Hiding all context menus below the initiating item
   bool              OnHideBackContextMenus(void);
  };
//+------------------------------------------------------------------+
//| ON_HIDE_BACK_CONTEXTMENUS event                                  |
//+------------------------------------------------------------------+
bool CWndEvents::OnHideBackContextMenus(void)
  {
//--- If the signal is to hide context menus below the initiating item
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_BACK_CONTEXTMENUS)
      return(false);
//--- Iterate over all menus from the last called
   int context_menus_total=CWndContainer::ContextMenusTotal(0);
   for(int i=context_menus_total-1; i>=0; i--)
     {
      //--- Pointers to the context menu and its previous node
      CContextMenu *cm=m_wnd[0].m_context_menus[i];
      CMenuItem    *mi=cm.PrevNodePointer();
      //--- If made it to the signal initiating item, then...
      if(mi.Id()==m_lparam)
        {
         //--- ...if its context menu has no focus, hide it
         if(!cm.MouseFocus())
            cm.Hide();
         //--- Stop the loop
         break;
        }
      else
        {
         //--- Hide the context menu
         cm.Hide();
        }
     }
//---
   return(true);
  }

The CWndEvents::OnHideBackContextMenus() method must be called in the method of handling custom events as shown below.

//+------------------------------------------------------------------+
//| CHARTEVENT_CUSTOM event                                          |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- If the signal is for minimizing the form
   if(OnWindowRollUp())
      return;
//--- If the signal is for maximizing the form
   if(OnWindowUnroll())
      return;
//--- If the signal is for hiding the context menus below the initiating item
   if(OnHideBackContextMenus())
      return;
  }

 


Preliminary Test of Event Handlers

After all changes have been introduced, compile all the files and load the program to the chart for testing. Now, when an independent menu item on the form is clicked on, its context menu will appear if this was hidden before and hide if this was open. Added to that, when a context menu is open, then the background color of the menu item will be fixed, that is will not change again if the mouse cursor is removed from its area as shown in the screenshot below. 

Fig. 1. Test of showing and hiding a context menu.

Fig. 1. Test of showing and hiding a context menu.

 

We are continuing to adjust the interaction of the user with the context menu. In majority of applications, when one or several context menus are open (one from another), when a mouse click takes place outside of their boundaries, they get closed at once. Here, we are going to replicate the same behavior. 

To be able to test this functionality fully, let us add another context menu to the interface of our EA. We will attach a context menu to the third item of the present context menu. For that, assign the third element the MI_HAS_CONTEXT_MENU type in the CProgram::CreateContextMenu1() method of creating the first context menu in the items_type[] array:

//--- Array of item types
   ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS]=
     {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_HAS_CONTEXT_MENU,
      MI_CHECKBOX,
      MI_CHECKBOX
     };

Now, let us create a method for the second context menu. Add the second instance of the CContextMenu class to the CProgram class and declare the CreateContextMenu2() method:

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

The second context menu will contain six items. Those will be two groups of radio items (MI_RADIOBUTTON), three items in each. Below is the code of this method. What is the difference between this method and the method of creating the first context menu? Please note how we obtain the pointer to the third item of the first context menu to which the second context menu has to be attached. The CContextMenu::ItemPointerByIndex() method designated to it was created earlier. As we are going to use default icons for the radio items, they do not require arrays. In the CContextMenu::AddItem() method instead of the path to the icons, pass empty values. A separation line is required here for visual separation the first group of radio items from the second one. Therefore, set this after the third (2) item in the list.

It was mentioned earlier and shown on a schematic that each group of radio items must have its own unique identifier. The default value of this parameter is 0. For that reason, assign the identifier equal to 1 to each radio item of the second group (in the loop from the third to the sixth). The CContextMenu class already contains the CContextMenu::RadioItemIdByIndex() method for setting the identifier.

Let us specify what radio items in each group have to be highlighted initially using the CContextMenu::SelectedRadioItem() method. In the code below, the second radio item (index 1) is highlighted in the first group and the third radio item (index 2)is highlighted in the second group.

//+------------------------------------------------------------------+
//| Creates context menu 2                                           |
//+------------------------------------------------------------------+
bool CProgram::CreateMI1ContextMenu2(void)
  {
//--- Six items in a context menu
#define CONTEXTMENU_ITEMS2 6
//--- Store the window pointer
   m_mi1_contextmenu2.WindowPointer(m_window);
//--- Store the pointer to the previous node
   m_mi1_contextmenu2.PrevNodePointer(m_mi1_contextmenu1.ItemPointerByIndex(2));
//--- Array of item names
   string items_text[CONTEXTMENU_ITEMS2]=
     {
      "ContextMenu 2 Item 1",
      "ContextMenu 2 Item 2",
      "ContextMenu 2 Item 3",
      "ContextMenu 2 Item 4",
      "ContextMenu 2 Item 5",
      "ContextMenu 2 Item 6"
     };
//--- Set up properties before creation
   m_mi1_contextmenu2.XSize(160);
   m_mi1_contextmenu2.ItemYSize(24);
   m_mi1_contextmenu2.AreaBackColor(C'240,240,240');
   m_mi1_contextmenu2.AreaBorderColor(clrSilver);
   m_mi1_contextmenu2.ItemBackColorHover(C'240,240,240');
   m_mi1_contextmenu2.ItemBackColorHoverOff(clrLightGray);
   m_mi1_contextmenu2.ItemBorderColor(C'240,240,240');
   m_mi1_contextmenu2.LabelColor(clrBlack);
   m_mi1_contextmenu2.LabelColorHover(clrWhite);
   m_mi1_contextmenu2.SeparateLineDarkColor(C'160,160,160');
   m_mi1_contextmenu2.SeparateLineLightColor(clrWhite);
//--- Add items to the context menu
   for(int i=0; i<CONTEXTMENU_ITEMS2; i++)
      m_mi1_contextmenu2.AddItem(items_text[i],"","",MI_RADIOBUTTON);
//--- Separation line after the third item
   m_mi1_contextmenu2.AddSeparateLine(2);
//--- Set a unique identifier (1) for the second group
   for(int i=3; i<6; i++)
      m_mi1_contextmenu2.RadioItemIdByIndex(i,1);
//--- Selecting radio items in both groups
   m_mi1_contextmenu2.SelectedRadioItem(1,0);
   m_mi1_contextmenu2.SelectedRadioItem(2,1);
//--- Create a context menu
   if(!m_mi1_contextmenu2.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Add the element pointer to the base
   CWndContainer::AddToElementsArray(0,m_mi1_contextmenu2);
   return(true);
  }

 

Calling of the CProgram::CreateContextMenu2() method is located in the CProgram::CreateTradePanel() method as for the rest of them.

 

 

Test of Several Context Menus and Fine Adjustment

The result of compiling files of the EA and loading it on to the chart will be as shown below.

Fig. 2. Test of several context menus.

Fig. 2. Test of several context menus.

 

If both context menus are open when clicking on the item, which brings up the first menu, both menus will be closed. This behavior is underlying the CWndEvents::OnHideBackContextMenus() method, which has been considered above. However, if we click on the chart of the form header, the context menus will not be closed. We are going to work on this.

The location of the mouse cursor (focus) is defined in the OnEvent() event handler of the context menu class (CContextMenu). Therefore, a signal for closing all open context menus in the main event handler (in the CWndEvents class) will be sent there too. This task has the following solution.

1. When the mouse movement event (CHARTEVENT_MOUSE_MOVE) takes place, the string parameter sparam contains the state of the left mouse button.

2. Then, after the mouse focus has been identified, we carry out a check of the current state of the context menu and the left mouse button. If the context menu has been activated and the button has been pressed, we move on to the following check where the current cursor location is identified in relation to this context menu and the previous node.

3. If the cursor is in the area of one of those, a signal for closing all context menus does not have to be sent. If the cursor is outside of the area of those elements, we have to check if there are any context menus that were open later.

4. For that, iterate over the list of this context menu to identify if this contains an item with its own context menu attached. If there is such an item, check if its context menu has been activated. If it turned out that the context menu has been activated, the cursor may be in its area. This means that a signal for closing all context menus from this element does not have to be sent. If it happens that the current context menu was open last and in all the menus before the conditions for sending a signal were not met, it definitely means that the cursor is outside of the areas of all activated context menus.

5. The ON_HIDE_CONTEXTMENUS custom event can be generated here.

As we can see, the key thing is that all the context menus have be closed only when the mouse cursor (if the left mouse button is pressed) is outside of the area of the last activated context menu and outside of the area of the item from which it was called.

The described logic is in the code below. The CContextMenu::CheckHideContextMenus() method was dedicated to that.

class CContextMenu : public CElement
  {
private:
   //--- Condition check for closing all context menus
   void              CheckHideContextMenus(void);
   //---
  };
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      if(!CElement::m_is_visible)
         return;
      //--- Get the focus
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- If the context menu is enabled and the left mouse button is pressed
      if(m_context_menu_state && sparam=="1")
        {
         //--- Condition check for closing all context menus
         CheckHideContextMenus();
         return;
        }
      //---
      return;
     }
  }
//+------------------------------------------------------------------+
//| Condition check for closing all context menus                    |
//+------------------------------------------------------------------+
void CContextMenu::CheckHideContextMenus(void)
  {
//--- Leave, if the cursor is in the context menu area or in the previous node area
   if(CElement::MouseFocus() || m_prev_node.MouseFocus())
      return;
//--- If the cursor is outside of the area of these elements, then ...
//    ... a check is required if there are open context menus which were activated after that
//--- For that iterate over the list of this context menu ...
//    ... for identification if there is a menu item containing a context menu
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- If there is such an item, check if its context menu is open.
      //    It this is open, do not send a signal for closing all context menus from this element as...
      //    ... it is possible that the cursor is in the area of the following one and this has to be checked.
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU)
         if(m_items[i].ContextMenuState())
            return;
     }
//--- Send a signal for hiding all context menus
   ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
  }

Now, the ON_HIDE_CONTEXTMENUS event has to be received in the main handler of the library under development in the CWndEvents class. Let us write a method designated to it and name it OnHideContextMenus(). It is rather simple as currently it only has to iterate over the private array of context menus and hide them. 

The declaration and implementation of the CWndEvents::OnHideContextMenus() method is in the code below:

class CWndEvents : public CWndContainer
  {
private:
   //--- Hiding all context menus
   bool              OnHideContextMenus(void);
  };
//+------------------------------------------------------------------+
//| ON_HIDE_CONTEXTMENUS event                                       |
//+------------------------------------------------------------------+
bool CWndEvents::OnHideContextMenus(void)
  {
//--- If the signal is for hiding all context menus
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS)
      return(false);
//---
   int cm_total=CWndContainer::ContextMenusTotal(0);
   for(int i=0; i<cm_total; i++)
      m_wnd[0].m_context_menus[i].Hide();
//---
   return(true);
  }

After compiling the library files and loading the EA to the chart for tests, you will see that activated context menus will be hidden if a mouse click takes place outside of their areas.

We have to eliminate another noticeable design flaw. Take a look at the screenshot below. It shows a situation when the mouse cursor is in the area of the first context menu but outside of the area of the menu item from which the second context menu is called. Usually, in such cases all context menus opened after the one where the cursor is currently located are closed. Let us write a code for it.

Fig. 3. In such a situation, all context menus on the right must be hidden.

Fig. 3. In such a situation, all context menus on the right must be hidden.

 

We will name the next method CContextMenu::CheckHideBackContextMenus(). Its logic was described in the previous paragraph and we can proceed straight to its implementation (see the code below). If all conditions are met, then the ON_HIDE_BACK_CONTEXTMENUS event is generated. 

class CContextMenu : public CElement
  {
private:
   //--- Condition check for closing all context menus which were open after this one
   void              CheckHideBackContextMenus(void);
   //---
  };
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      if(!CElement::m_is_visible)
         return;
      //--- Get the focus
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- If the context menu is enabled and the left mouse button is pressed
      if(m_context_menu_state && sparam=="1")
        {
         //--- Condition check for closing all context menus
         CheckHideContextMenus();
         return;
        }
      //--- Condition check for closing all context menus which were open after this one
      CheckHideBackContextMenus();
      return;
     }
  }
//+------------------------------------------------------------------+
//| Checking conditions for closing all context menus                |
//| which were open after this one                                   |
//+------------------------------------------------------------------+
void CContextMenu::CheckHideBackContextMenus(void)
  {
//--- Iterate over all menu items
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- If the item contains a context menu and this is enabled
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU && m_items[i].ContextMenuState())
        {
         //--- If the focus is in the context menu but not in this item
         if(CElement::MouseFocus() && !m_items[i].MouseFocus())
            //--- Send a signal to hide all context menus which were open after this one
            ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,"");
        }
     }
  }

Earlier the OnHideBackContextMenus() method was written in the CWndEvents class for handling the ON_HIDE_BACK_CONTEXTMENUS event, therefore, the files of the project can be compiled and the EA tested. If everything was done correctly, the context menus will react to moving the mouse cursor in accordance with the program requirements.

The most difficult part is over but the job is not finished yet. The event handlers have to be set up in the way that when any of the context menu items is clicked on, a message with parameter values is sent to the custom class of the application (CProgram). These parameters will allow to identify what menu item exactly was clicked on. This way, the developer of the application can assign certain functions to menu items. Besides, switching the sates of checkboxes and radio items of the context menu is still to be set up.

The block for the condition when a menu item does not contain a context menu but is a part of it is still empty in the OnClickMenuItem() method of the CMenuItem class. The ON_CLICK_MENU_ITEM custom event will be sent from here. The message will contain the following additional parameters:

  1. The index of the common list. 
  2. The element identifier.
  3. A line that will be formed from:

  • the program name;
  • checkbox or radio item indication;
  • in case this is a radio item, the line will also contain the radio item identifier.

As you can see, when the EventChartCustom() function is not sufficient, a string with the required number of parameters for exact identification can always be formed. Similar to the names of graphical objects, parameters will be divided by the underscore "_".

The state of the checkbox and radio item will also be changed in the same block. Below is a shortened version of the CMenuItem::OnClickMenuItem() method. It shows only the code that must be added to the block else.

//+------------------------------------------------------------------+
//| Clicking on the element header                                   |
//+------------------------------------------------------------------+
bool CMenuItem::OnClickMenuItem(const string clicked_object)
  {
//--- Check by the object name
//--- Leave, if the item has not been activated
//--- If this item contains a context menu
      //... 
//--- If this item does not contain a context menu, but is a part of a context menu itself
   else
     {
      //--- Message prefix with the program name
      string message=CElement::ProgramName();
      //--- If this is a checkbox, change its state
      if(m_type_menu_item==MI_CHECKBOX)
        {
         m_checkbox_state=(m_checkbox_state)? false : true;
         m_icon.Timeframes((m_checkbox_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
         //--- Add to the message that this is a checkbox
         message+="_checkbox";
        }
      //--- If this is a radio item, change its state
      else if(m_type_menu_item==MI_RADIOBUTTON)
        {
         m_radiobutton_state=(m_radiobutton_state)? false : true;
         m_icon.Timeframes((m_radiobutton_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
         //--- Add to the message that this is a radio item
         message+="_radioitem_"+(string)m_radiobutton_id;
        }
      //--- Send a message about it
      ::EventChartCustom(m_chart_id,ON_CLICK_MENU_ITEM,m_index,CElement::Id(),message);
     }
//---
   return(true);
  }

A custom event with the ON_CLICK_MENU_ITEM identifier is designated to the handler of the context menu class (CContextMenu). We will need additional methods for extracting the identifier from the string parameter of the event if the click was on the radio item and also for getting the index in relation to the group this radio item belongs to. You can see the code of those methods below.

As the extraction of the identifier from the string parameter depends on the structure of the passed string, the CContextMenu::RadioIdFromMessage() method will contain additional checks for correctness of the formed string and exceeding the array size.

Get the radio item identifier by the general index at the beginning of the CContextMenu::RadioIndexByItemIndex() method, which is dedicated to returning the radio item index by the general index. Use the CContextMenu::RadioItemIdByIndex() method written earlier. After that, count radio items with this identifier in the loop. Having made it to the radio item with the general index the value of which is equal to the passed index, store the value of the counter and stop the loop. This means that the last value of the counter will be the index that has to be returned.

class CContextMenu : public CElement
  {
private:
   //--- Getting (1) the identifier and (2) index from the radio item message
   int               RadioIdFromMessage(const string message);
   int               RadioIndexByItemIndex(const int index);
   //---
  };
//+------------------------------------------------------------------+
//| Extracts the identifier from the message for the radio item      |
//+------------------------------------------------------------------+
int CContextMenu::RadioIdFromMessage(const string message)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Get the code of the separator
   u_sep=::StringGetCharacter("_",0);
//--- Split the string
   ::StringSplit(message,u_sep,result);
   array_size=::ArraySize(result);
//--- If the message structure differs from the expected one
   if(array_size!=3)
     {
      ::Print(__FUNCTION__," > Wrong structure in the message for the radio item! message: ",message);
      return(WRONG_VALUE);
     }
//--- Prevention of exceeding the array size
   if(array_size<3)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//--- Return the radio item id
   return((int)result[2]);
  }
//+------------------------------------------------------------------+
//| Returns the radio item index by the general index                |
//+------------------------------------------------------------------+
int CContextMenu::RadioIndexByItemIndex(const int index)
  {
   int radio_index =0;
//--- Get the radio item id by the general index
   int radio_id =RadioItemIdByIndex(index);
//--- Item counter from the required group
   int count_radio_id=0;
//--- Iterate over the list
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- If this is not a radio item, move to the next one
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- If identifiers match
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- If the indices match 
         //    store the current counter value and complete the loop
         if(m_items[i].Index()==index)
           {
            radio_index=count_radio_id;
            break;
           }
         //--- Increase the counter
         count_radio_id++;
        }
     }
//--- Return the index
   return(radio_index);
  }

Now, let us crate the CContextMenu::ReceiveMessageFromMenuItem() method for handling the ON_CLICK_MENU_ITEM custom event from the menu item. The following event parameters must be passed to this method: the identifier, index and string message. Conditions whether this message was received from our program and whether the identifiers match are checked at the beginning of this method. If the check is positive and if this message was sent from a radio item, the switch is carried out in the group that is defined by the identifier and the required item by the index. The identifier and index can be obtained with the help of the methods created above. 

Regardless of the menu item type from which a message came from, in case the check of the program name and comparison of identifiers is successful, the ON_CLICK_CONTEXTMENU_ITEM custom message is sent. It is addressed to the handler in the CProgram class of the custom application. Together with the message, the following parameters are sent: (1) identifier, (2) general index in the list of the context menu (3) displayed text of the item.

At the end of the method, regardless of the first check (1) the context menu is hidden, (2) the form is unblocked (3) and a signal for closing all context menus is sent.

class CContextMenu : public CElement
  {
private:
   //--- Receiving a message from the menu item for handling
   void              ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item);
   //---
  };
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the ON_CLICK_MENU_ITEM event
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
     {
      int    item_id      =int(dparam);
      int    item_index   =int(lparam);
      string item_message =sparam;
      //--- Receiving a message from the menu item for handling
      ReceiveMessageFromMenuItem(item_id,item_index,item_message);
      return;
     }
  }
//+------------------------------------------------------------------+
//| Receiving a message from the menu item for handling              |
//+------------------------------------------------------------------+
void CContextMenu::ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item)
  {
//--- If there is an indication that the message was received from this program and the element id matches
   if(::StringFind(message_item,CElement::ProgramName(),0)>-1 && id_item==CElement::Id())
     {
      //--- If clicking was on the radio item
      if(::StringFind(message_item,"radioitem",0)>-1)
        {
         //--- Get the radio item id from the passed message
         int radio_id=RadioIdFromMessage(message_item);
         //--- Get the radio item index by the general index
         int radio_index=RadioIndexByItemIndex(index_item);
         //--- Switch the radio item
         SelectedRadioItem(radio_index,radio_id);
        }
      //--- Send a message about it
      ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,index_item,id_item,DescriptionByIndex(index_item));
     }
//--- Hide the context menu
   Hide();
//--- Unblock the form
   m_wnd.IsLocked(false);
//--- Send a signal for hiding all context menus
   ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
  }

 

 


Test of Receiving Messages in the Custom Class of the Application

Now, we can test receiving such a message in the handler of the CProgram class. For that, add the code to it as shown below:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_CONTEXTMENU_ITEM)
     {
      ::Print(__FUNCTION__," > index: ",lparam,"; id: ",int(dparam),"; description: ",sparam);
     }
  }

Now, compile the files and load the EA on to the chart. When menu items are clicked on, messages with parameters of those items will be printed in the journal of the EA:

2015.10.23 20:16:27.389 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 4; id: 2; description: ContextMenu 1 Item 5
2015.10.23 20:16:10.895 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 0; id: 3; description: ContextMenu 2 Item 1
2015.10.23 19:27:58.520 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 5; id: 3; description: ContextMenu 2 Item 6
2015.10.23 19:27:26.739 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 2; id: 3; description: ContextMenu 2 Item 3
2015.10.23 19:27:23.351 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 3; id: 3; description: ContextMenu 2 Item 4
2015.10.23 19:27:19.822 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 4; id: 2; description: ContextMenu 1 Item 5
2015.10.23 19:27:15.550 TestLibrary (USDCAD,D1) CProgram::OnEvent > index: 1; id: 2; description: ContextMenu 1 Item 2

We have completed the development of the main part of the CContextMenu class for creating a context menu. It will require some additions later but we will return to this when problems manifest themselves at tests. In short, we will follow natural sequence of narration as this way it is easier to study the material.

 


Conclusion

In this article, we have enriched the classes of elements created in the previous articles. Now, we have everything ready for developing the main menu element. We will work on it in the next article.

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 articles (chapters) of the second part: