Graphical Interfaces II: Setting Up the Event Handlers of the Library (Chapter 3)
Contents
- Introduction
- Private Arrays of the Elements
- Managing the State of the Chart
- Identifiers for External and Internal Use
- Enriching the Class of the Context Menu
- Enriching the Class of the Menu Item
- Enriching the Main Class of Handling Events of the Graphical Interface
- Preliminary Test of Event Handlers
- Test of Several Context Menus and Fine Adjustment
- Test of Receiving Messages in the Custom Class of the Application
- Conclusion
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:
- Private arrays for each meaningful control.
- Adding element pointers to the base. These elements are constituent parts of complex (compound) elements.
- Managing the state of the chart depending on the location of the mouse cursor.
- Identifiers of the library events for internal and external use.
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:
- ON_CLICK_MENU_ITEM — clicking on the menu item.
- ON_HIDE_CONTEXTMENUS — signal for hiding all context menus.
- ON_HIDE_BACK_CONTEXTMENUS — signal for hiding the context menus below the current menu item. We will discuss this in detail later.
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:
- For setting and getting the context menu state.
- For handling of the clicking on the menu item event.
- For getting the identifier and index from the menu item name. We already know that that is the very reason why the index and identifier are part of the names of the objects that constitute various elements.
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.
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.
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.
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:
- The index of the common list.
- The element identifier.
- 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:
- Graphical Interfaces II: the Menu Item Element (Chapter 1)
- Graphical Interfaces II: the Separation Line and Context Menu Elements (Chapter 2)
- Graphical Interfaces II: Setting Up the Event Handlers of the Library (Chapter 3)
- Graphical Interfaces II: The Main Menu Element (Chapter 4)
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/2204
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
It's really good work.
In the constructor of CContextMenu I added the line
CContextMenu::m_item_y_size=24;
so 'm_item_y_size' has a default value and the TestLibrary compiles pretty.
It's really good work.
In the constructor of CContextMenu I added the line
so 'm_item_y_size' has a default value and the TestLibrary compiles pretty.