Graphical Interfaces II: The Main Menu Element (Chapter 4)
Contents
- Introduction
- Developing the Class for Creating the Main Menu
- Test of Setting Up the Main Menu
- Blocking Inactive Controls
- Methods for Communication with the Main Menu
- Final Test of the Main Menu
- Conclusion
Introduction
The first article Graphical Interfaces I: Preparation of the Library Structure (Chapter 1) considers in detail what this library is for. A full list of the links to the articles is at the end of each chapter of the series. There you can also find and download a complete version of the library at the current stage of development. The files must be located in the same directories as in the archive.
This is the final chapter of the second part of the series about graphical interfaces. Here, we are going to consider the creation of the main menu. The development of this control and setting up handlers of the library classes for correct reaction to the user's actions will be demonstrated here. We will also discuss how to attach context menus to the items of the main menu. In addition, we will speak about blocking the elements that are currently inactive.
Developing the Class for Creating the Main Menu
Classes for creating all elements for building the main menu of the program were developed in the previous three chapters. We have the following classes:
- CMenuItem – menu item.
- CSeparateLine – separation line.
- CContextMenu – context menu.
Create the MenuBar.mqh file in the Controls folder located in the directory containing the files of all elements. In this file, include the file containing the base class, file with the form class and files of all compound elements that will constitute it:
//+------------------------------------------------------------------+ //| MenuBar.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "Element.mqh" #include "Window.mqh" #include "MenuItem.mqh" #include "ContextMenu.mqh"
The basic objects of the main menu are the background and menu items. Context menus will be attached to the items of the main menu via pointers.
Fig. 1. Basic pats of the main menu.
The initial form of the CMenuBar class with instances of necessary classes, pointers and virtual methods standard for each element is presented in the code below.
//+------------------------------------------------------------------+ //| Class for creating the main menu | //+------------------------------------------------------------------+ class CMenuBar : public CElement { private: //--- Pointer to the form to which the element is attached CWindow *m_wnd; //--- Objects for creating a menu item CRectLabel m_area; CMenuItem m_items[]; //--- Array of context menu pointers CContextMenu *m_contextmenus[]; //--- public: CMenuBar(void); ~CMenuBar(void); //--- Stores the form pointer void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } //--- Chart event handler virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam); //--- Moving the element virtual void Moving(const int x,const int y); //--- (1) Show, (2) hide, (3) reset, (4) delete virtual void Show(void); virtual void Hide(void); virtual void Reset(void); virtual void Delete(void); //--- (1) Setting, (2) resetting of priorities for left clicking virtual void SetZorders(void); virtual void ResetZorders(void); //--- };
Similar to any other interface element, there must be a possibility to set up the appearance of the main menu. Fields and methods will be created specifically for that purpose and will allow to set up:
- background properties of the main menu;
- general properties of the menu items.
Besides, methods for setting and getting the current state of the main menu will be required. All the fields have to be initialized in the constructor by default values. The default height of the main menu will be 22 pixels. The height of the menu items will be calculated automatically in relation to the background height of the main menu. This value, however, can be altered before attaching the element to the chart with the help of the CElement::YSize() method of the base class. The background width of the main menu is equal to the width of the form to which this is attached. This is why we will make the calculation of this parameter automated when attaching the element to the chart.
class CMenuBar : public CElement { private: //--- Background properties int m_area_zorder; color m_area_color; color m_area_color_hover; color m_area_border_color; //--- General properties of menu items int m_item_y_size; color m_item_color; color m_item_color_hover; color m_item_border_color; int m_label_x_gap; int m_label_y_gap; color m_label_color; color m_label_color_hover; //--- State of the main menu bool m_menubar_state; //--- public: //--- Color of (1) the background and (2) the background frame of the main menu void MenuBackColor(const color clr) { m_area_color=clr; } void MenuBorderColor(const color clr) { m_area_border_color=clr; } //--- (1) background color, (2) background color when the cursor is hovering over it and (3) frame color of the main menu items void ItemBackColor(const color clr) { m_item_color=clr; } void ItemBackColorHover(const color clr) { m_item_color_hover=clr; } void ItemBorderColor(const color clr) { m_item_border_color=clr; } //--- Margins of the text label from the edge point of the menu item background void LabelXGap(const int x_gap) { m_label_x_gap=x_gap; } void LabelYGap(const int y_gap) { m_label_y_gap=y_gap; } //--- (1) Standard and (2) in-focus text color void LabelColor(const color clr) { m_label_color=clr; } void LabelColorHover(const color clr) { m_label_color_hover=clr; } //--- State of the main menu void State(const bool state); bool State(void) const { return(m_menubar_state); } //--- }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMenuBar::CMenuBar(void) : m_menubar_state(false), m_area_zorder(0), m_area_color(C'240,240,240'), m_area_border_color(clrSilver), m_item_color(C'240,240,240'), m_item_color_hover(C'51,153,255'), m_item_border_color(C'240,240,240'), m_label_x_gap(15), m_label_y_gap(3), m_label_color(clrBlack), m_label_color_hover(clrWhite) { //--- Store the name of the element class in the base class CElement::ClassName(CLASS_NAME); //--- Default height of the main menu m_y_size=22; } //+------------------------------------------------------------------+ //| Setting the state of the main menu | //+------------------------------------------------------------------+ void CMenuBar::State(const bool state) { if(state) m_menubar_state=true; else { m_menubar_state=false; //--- Iterate over all items of the main menu for // setting the status of disabled context menus int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) m_items[i].ContextMenuState(false); //--- Unblock the form m_wnd.IsLocked(false); } }
Setting up unique properties for each menu item requires array. Unique properties are:
- width of the menu item;
- displayed text.
These properties will be set up before attaching the main menu to the chart, that is during its formation at the moment of adding every item. For that, the CMenuBar::AddItem() method will be used similar to the one created earlier in the CContextMenu class. The only difference between them is passed (set) parameters.
Let us create the CMenuBar::AddContextMenuPointer() method for attaching context menus to every item of the main menu. The index of the main menu item and the object of the context menu are to be passed to this method. The pointer to the object of the context menu will be stored in the m_contextmenus[] array.
class CMenuBar : public CElement { private: //--- Arrays of unique properties of menu items: // (1) Width, (2) text int m_width[]; string m_label_text[]; //--- public: //--- Adds a menu item with specified properties before creation of the main menu void AddItem(const int width,const string text); //--- Attaches the passed context menu to the specified item of the main menu void AddContextMenuPointer(const int index,CContextMenu &object); //--- }; //+------------------------------------------------------------------+ //| Adds a menu item | //+------------------------------------------------------------------+ void CMenuBar::AddItem(const int width,const string text) { //--- Increase the size of the arrays by one element int array_size=::ArraySize(m_items); ::ArrayResize(m_items,array_size+1); ::ArrayResize(m_contextmenus,array_size+1); ::ArrayResize(m_width,array_size+1); ::ArrayResize(m_label_text,array_size+1); //--- Store the values of passed parameters m_width[array_size] =width; m_label_text[array_size] =text; } //+------------------------------------------------------------------+ //| Adds the context menu pointer | //+------------------------------------------------------------------+ void CMenuBar::AddContextMenuPointer(const int index,CContextMenu &object) { //--- Check for exceeding the range int size=::ArraySize(m_contextmenus); if(size<1 || index<0 || index>=size) return; //--- Store the pointer m_contextmenus[index]=::GetPointer(object); }
We will also require the methods for getting the pointer to the main menu item and the pointer to one of the context menus attached to the items. Each of these methods will contain the check of the array size and adjustment of the index in case the array range has been exceeded. In addition to that, the iteration over items of the main menu and context menus will be carried out a lot. This is why methods for getting the sizes of their arrays are required.
class CMenuBar : public CElement { public: //--- (1) Getting the pointer to the specified menu item, (2) getting the pointer to the specified context menu CMenuItem *ItemPointerByIndex(const int index); CContextMenu *ContextMenuPointerByIndex(const int index); //--- Quantity of (1) menu items and (2) context menus int ItemsTotal(void) const { return(::ArraySize(m_items)); } int ContextMenusTotal(void) const { return(::ArraySize(m_contextmenus)); } //--- }; //+------------------------------------------------------------------+ //| Returns a menu item pointer by the index | //+------------------------------------------------------------------+ CMenuItem *CMenuBar::ItemPointerByIndex(const int index) { int array_size=::ArraySize(m_items); //--- If the main menu does not contain any item, report if(array_size<1) { ::Print(__FUNCTION__," > This method is to be called, " "when the main menu contains at least one item!"); } //--- Adjustment in case the range has been exceeded int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index; //--- Return the pointer return(::GetPointer(m_items[i])); } //+------------------------------------------------------------------+ //| Returns the context menu pointer by the index | //+------------------------------------------------------------------+ CContextMenu *CMenuBar::ContextMenuPointerByIndex(const int index) { int array_size=::ArraySize(m_contextmenus); //--- If the main menu does not contain any item, report if(array_size<1) { ::Print(__FUNCTION__," > This method is to be called, " "when the main menu contains at least one item!"); } //--- Adjustment in case the range has been exceeded int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index; //--- Return the pointer return(::GetPointer(m_contextmenus[i])); }
The process of building the main menu does not have any fundamental differences from the creation of the context menu in the CContextMenu class. Actually, the creation of the main menu is a little simpler as the main menu does not require the pointer to the previous node. This does not contain separation lines either. We will not consider the code of those methods to save space in the article. You can look them up in the MenuBar.mqh file attached at the end of the article.
Earlier, to ensure that the context menu items get to the base of all elements in the CWndContainer class, the special method AddContextMenuElements() was written. This is called in the CWndContainer::AddToElementsArray() main method of adding elements to the base. The same method must be written for the main menu element, otherwise the menu items will not move together with the form and will not change their color when the mouse cursor is hovering over them.
Below is a short list of actions to perform for the compound elements of some main element to get to the base and the main element pointer to get to the private array.
- Include the file with the element class to the WndContainer.mqh file.
- Add the element array to the WindowElements structure.
- Add the method for getting the number of the main menus in the private array.
- Declare and implement the private method for storing the pointers to the elements that are a part of another main element.
- Place the call of the new method in the common method which is dedicated to being used in the application class for adding the main element pointer to the base.
Below is a shortened version of the code from the WndContainer.mqh file which presents only what needs to be added to it:
//+------------------------------------------------------------------+ //| WndContainer.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "MenuBar.mqh" //+------------------------------------------------------------------+ //| Class for storing all interface objects | //+------------------------------------------------------------------+ class CWndContainer { protected: //--- Structure of the element arrays struct WindowElements { //--- Main menu array CMenuBar *m_menu_bars[]; }; //--- public: //--- Number of main menus int MenuBarsTotal(const int window_index); //--- private: //--- Stores pointers to the main menu elements in the base bool AddMenuBarElements(const int window_index,CElement &object); }; //+------------------------------------------------------------------+ //| Returns the number of main menus by the specified window index | //+------------------------------------------------------------------+ int CWndContainer::MenuBarsTotal(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_menu_bars)); } //+------------------------------------------------------------------+ //| Adds a pointer to the element array | //+------------------------------------------------------------------+ void CWndContainer::AddToElementsArray(const int window_index,CElement &object) { //--- If the base does not contain forms for controls //--- If the request is for a non-existent form //--- Add to the common array of elements //--- Add element objects to the common array of objects //--- Store the id of the last element in all forms //--- Increase the counter of element identifiers //--- Stores the pointers to the context menu objects in the base //--- Stores the pointers to the main menu objects in the base if(AddMenuBarElements(window_index,object)) return; } //+------------------------------------------------------------------+ //| Stores the pointers to the main menu objects in the base | //+------------------------------------------------------------------+ bool CWndContainer::AddMenuBarElements(const int window_index,CElement &object) { //--- Leave, if this is not the main menu if(object.ClassName()!="CMenuBar") return(false); //--- Get the main menu pointer CMenuBar *mb=::GetPointer(object); //--- Store the pointers to its objects in the base int items_total=mb.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=mb.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(mb,m_wnd[window_index].m_menu_bars); return(true); }
Test of Setting Up the Main Menu
At this stage we can test setting up the main menu. We are going to build this element from three items. The width and displayed text for each of the elements are going to be set up while the rest of the properties are going to stay as default.
Add the code for creating the main menu to the CProgram application class as shown in the code below. Adjust the coordinates of other elements that were attached to the form prior to that. And finally, place the call of the CProgram::CreateMenuBar() new method in the main method for creating the graphical interface.
class CProgram : public CWndEvents { private: //--- Main menu CMenuBar m_menubar; //--- private: //--- #define MENUBAR_GAP_X (1) #define MENUBAR_GAP_Y (20) bool CreateMenuBar(void); //--- #define MENU_ITEM1_GAP_X (6) #define MENU_ITEM1_GAP_Y (45) //--- #define SEP_LINE_GAP_X (6) #define SEP_LINE_GAP_Y (75) //--- }; //+------------------------------------------------------------------+ //| Creates the trading panel | //+------------------------------------------------------------------+ bool CProgram::CreateTradePanel(void) { //--- Creating a form for controls //--- Creating controls: // Main menu if(!CreateMenuBar()) return(false); //--- Menu item //--- Separation line //--- Redrawing of the chart return(true); } //+------------------------------------------------------------------+ //| Creates the main menu | //+------------------------------------------------------------------+ bool CProgram::CreateMenuBar(void) { //--- Three items in the main menu #define MENUBAR_TOTAL 3 //--- Store the window pointer m_menubar.WindowPointer(m_window); //--- Coordinates int x=m_window.X()+MENUBAR_GAP_X; int y=m_window.Y()+MENUBAR_GAP_Y; //--- Arrays with unique properties of each item int width[MENUBAR_TOTAL] ={50,55,53}; string text[MENUBAR_TOTAL] ={"File","View","Help"}; //--- Add items to the main menu for(int i=0; i<MENUBAR_TOTAL; i++) m_menubar.AddItem(width[i],text[i]); //--- Create a control if(!m_menubar.CreateMenuBar(m_chart_id,m_subwin,x,y)) return(false); //--- Add the object to the common array of the object groups CWndContainer::AddToElementsArray(0,m_menubar); return(true); }
Compile the files and load the EA on to the chart. The result should be as in the screenshot below. The main menu will be moving together with the form and its items will change their color when the mouse cursor is hovering over them.
Fig. 2. Test of the main menu element.
Blocking Inactive Controls
Before creating context menus and attaching them to the items of the main menu, our library requires functionality that will block the form and other elements when one of the elements is activated. Why does this have to be done? Activated elements here are the ones that are made visible (called) through other elements and are hidden when they are not needed any more. For instance, drop-down lists, context menus, calendars and the likes belong to this group. Try to activate any context menu or a drop-down list in the MetaTrader trading terminal. You will see that when an element of this type is activated, other controls in the whole terminal become unavailable. It is manifested, for example, when they do not change their color when the mouse cursor is hovering over them. The reason for such blocking is to exclude a situation when the cursor reacts to an element that is currently obscured by a drop-down list.
All you need to do to replicate this behavior is to block the form to which the activated control belongs. Any other element on this form has access to it via the pointer and, therefore, it can be found out what state it is in at any time. The principle here is very simple - if the form is blocked, then the method for changing the element color does not need to be called.
Add a special field and methods for setting and getting the state of the form (blocked/unblocked) to the CWindow class for creating a form as shown in the code below. The m_is_locked field in the constructor must be initialized by the false value. This means that the form must be unblocked by default. We can add the condition defining that the color of the form and its elements must take place only when the form is not blocked.
class CWindow : public CElement { private: //--- Status of a blocked window bool m_is_locked; //--- public: //--- Status of a blocked window bool IsLocked(void) const { return(m_is_locked); } void IsLocked(const bool flag) { m_is_locked=flag; } //--- }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CWindow::CWindow(void) : m_is_locked(false) { } //+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void CWindow::OnEventTimer(void) { //--- If the window is not blocked if(!m_is_locked) { //--- Changing the color of the form objects ChangeObjectsColor(); } }
As far as other controls are concerned, we currently have only one clickable element that we can test the functionality on - a menu item. Changing the color of the menu item when the mouse cursor is hovering over it will depend on the state of the form to which it is attached. Therefore, such a check must be placed also in its class CMenuItem as shown in the code below.
//+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void CMenuItem::OnEventTimer(void) { //--- If the window is available if(!m_wnd.IsLocked()) { //--- If the status of a disabled context menu if(!m_context_menu_state) //--- Changing the color of the form objects ChangeObjectsColor(); } }
The form must be blocked at the moment when the context menu becomes visible, that is in the CContextMenu::Show() method.
//+------------------------------------------------------------------+ //| Shows a context menu | //+------------------------------------------------------------------+ void CContextMenu::Show(void) { //--- Leave, if the element is already visible //--- Show the objects of the context menu //--- Show menu items //--- Assign the status of a visible element //--- State of the context menu //--- Register the state in the previous node //--- Block the form m_wnd.IsLocked(true); }
It may seem that calling the CWindow::IsLocked() method in the CContextMenu::Hide() method is sufficient for unblocking. This option is not suitable as several context menus can be open at the same time. Not all of them are closed simultaneously. Let us recall in what cases open context menus are closed altogether. Some conditions have to be met for that. For instance, in the CContextMenu::CheckHideContextMenus() method, when after the check of all conditions, a signal for closing all context menus is sent. The second case, in the CContextMenu::ReceiveMessageFromMenuItem() method when the ON_CLICK_MENU_ITEM event is being handled.
We are going to add unblocking of the form to these methods. Below are the shortened versions of the methods. The comments will help you to identify where the code highlighted in yellow has to be added.
//+------------------------------------------------------------------+ //| 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 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 //--- Unblock the form m_wnd.IsLocked(false); //--- Send a signal for hiding all context menus } //+------------------------------------------------------------------+ //| 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 //--- Hide the context menu //--- Unblock the form m_wnd.IsLocked(false); }
If at this stage all files are compiled and the EA we tested earlier is loaded on to the chart, we will see immediately after the context menu is open that everything is working quite differently to what we expected. All menu items, even the ones that are in the activated context menu, will be prevented from changing color. This should not be happening. In such cases, a context menu should have its own method for changing the color of its items. Let us create such a method in the CContextMenu class and locate its call in the timer as shown in the code below.
class CContextMenu : public CElement { public: //--- Changes the color of menu items when the cursor is hovering over them void ChangeObjectsColor(void); }; //+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void CContextMenu::OnEventTimer(void) { //--- Changing the color of menu items when the cursor is hovering over them ChangeObjectsColor(); } //+------------------------------------------------------------------+ //| Changing the object color when the cursor is hovering over it | //+------------------------------------------------------------------+ void CContextMenu::ChangeObjectsColor(void) { //--- Leave, if the context menu is disabled if(!m_context_menu_state) return; //--- Iterate over all menu items int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Change the color of the menu item m_items[i].ChangeObjectsColor(); } }
Now, everything should be working as designed.
Fig. 3. Test of blocking the form and all controls except the currently activated one.
Methods for Communication with the Main Menu
We are going to continue developing the CMenuBar class for creating the main menu. The remaining part concerns managing this element. Let us consider in detail how the main menu works with the examples of other programs. After the program is loaded, the main menu is deactivated by default. In this state, when the mouse cursor is hovering over the main menu items, they are just highlighted. When one of the item is clicked on, the main menu is activated and the context menu appears from the item that was clicked on. When the main menu is activated this way, if the mouse cursor is moving along the menu items, the context menus will be automatically switching depending on what item the mouse cursor is hovering over.
Before we implement such a behavior in our library, let us create three context menus and attach them to the items of the main menu. To save space, we will consider a shortened version of one of them. You can see complete files of the EA at the end of the article.
The code below contains only significant lines of code. Please note how the pointer to the previous node is passed to the context menu and the context menu pointer is stored in the main menu. The calculation of the coordinates from the lower part of the item must be set up when the properties of the context menus for the main menu are set up. In every other respect, the creation of the context menu is not different from what was considered earlier.
//+------------------------------------------------------------------+ //| Creates a context menu | //+------------------------------------------------------------------+ bool CProgram::CreateMBContextMenu1(void) { //--- Three items in the context menu //--- Store the window pointer m_mb_contextmenu1.WindowPointer(m_window); //--- Store the pointer to the previous node m_mb_contextmenu1.PrevNodePointer(m_menubar.ItemPointerByIndex(0)); //--- Attach the context menu to the specified menu item m_menubar.AddContextMenuPointer(0,m_mb_contextmenu1); //--- Array of item names //--- Label array for the available mode //--- Label array for the blocked mode //--- Array of item types //--- Set up properties before creation m_mb_contextmenu1.FixSide(FIX_BOTTOM); //--- Add items to the context menu //--- Separation line after the second item //--- Deactivate the second item //--- Create a context menu if(!m_mb_contextmenu1.CreateContextMenu(m_chart_id,m_subwin)) return(false); //--- Add the object to the common array of the object groups CWndContainer::AddToElementsArray(0,m_mb_contextmenu1); return(true); }
Now, let us set up the event handlers in the СMenuBar class of the main menu. We are going to start with handling of clicking on the menu items. Before that, similar to the CMenuItem and CContextMenu classes, we will require the OnClickMenuItem() method and methods designated to extracting the index and menu item identifier from the name of the object that was clicked on.
The methods for identification are the same as in the CContextMenu class. However, handling of clicking on the menu item has its peculiarities in the СMenuBar class. Check of identifier is followed by a check for correctness of the context menu pointer by the index obtained from the object name. If there is no pointer, a signal for closing all open context menus is sent. If there is a pointer, then a signal for closing all context menus is sent only in the case if clicking on the item was for closing the current context menu.
class CMenuBar : public CElement { private: //--- Handling clicking on the menu item 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); //--- }; //+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Handling of the left mouse click event on the main menu item if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickMenuItem(sparam)) return; } } //+------------------------------------------------------------------+ //| Clicking on the main menu item | //+------------------------------------------------------------------+ bool CMenuBar::OnClickMenuItem(const string clicked_object) { //--- 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 identifier does not match if(id!=CElement::Id()) return(false); //--- If there is a context menu pointer if(CheckPointer(m_contextmenus[index])!=POINTER_INVALID) { //--- The state of the main menu depends on the visibility of the context menu m_menubar_state=(m_contextmenus[index].ContextMenuState())? false : true; //--- Set the state of the form m_wnd.IsLocked(m_menubar_state); //--- If the main menu is disabled if(!m_menubar_state) //--- Send a signal for hiding all context menus ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,""); } //--- If there is no context menu pointer else { //--- Send a signal for hiding all context menus ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,""); } //--- return(true); }
As we can remember, handling of the ON_HIDE_CONTEXTMENUS event is carried out in the CWndEvents class. We need to add to the CWndEvents::OnHideContextMenus() method one more cycle which will enforce switching off all main menus that are present in the base.
//+------------------------------------------------------------------+ //| 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); //--- Hide all context menus int cm_total=CWndContainer::ContextMenusTotal(0); for(int i=0; i<cm_total; i++) m_wnd[0].m_context_menus[i].Hide(); //--- Disable main menus int menu_bars_total=CWndContainer::MenuBarsTotal(0); for(int i=0; i<menu_bars_total; i++) m_wnd[0].m_menu_bars[i].State(false); //--- return(true); }
If we compile all files and load the EA on to the chart, context menus will open when the main menu items are clicked on and will close after the second click.
Fig. 4. Test of calling context menus from the main menu.
Then, we are going to implement the methods that will allow us to switch context menus by moving the mouse cursor when the main menu is activated, similar to the way it is implemented in Windows applications. For that, we will need a method for highlighting the main menu items, when it is activated and an auxiliary method for defining an active item (the in-focus item) of activated main menu.
class CMenuBar : public CElement { public: //--- Changes the color when the mouse cursor is hovering over it void ChangeObjectsColor(void); //--- private: //--- Returns the active item of the main menu int ActiveItemIndex(void); //--- }; //+------------------------------------------------------------------+ //| Changing the object color when the cursor is hovering over it | //+------------------------------------------------------------------+ void CMenuBar::ChangeObjectsColor(void) { int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) m_items[i].ChangeObjectsColor(); } //+------------------------------------------------------------------+ //| Returns the index of the activated menu item | //+------------------------------------------------------------------+ int CMenuBar::ActiveItemIndex(void) { int active_item_index=WRONG_VALUE; //--- int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- If the item is in focus if(m_items[i].MouseFocus()) { //--- Store the index and stop the loop active_item_index=i; break; } } //--- return(active_item_index); }
Besides, we are going to create a method that will facilitate switching of context menus by hovering the cursor over when the main menu is activated. Let us name this method SwitchContextMenuByFocus(). The index of activated item of the main menu is to be passed to this method. This index will allow to define which context menu has to be made visible. All other context menus are hidden. At the same time, a check is carried out if there are any open context menus that are called from the main menu items. If it turns out that there are such menus, then the ON_HIDE_BACK_CONTEXTMENUS custom event is generated. We have already considered this event in detail in this article.
After the context menu has been hidden, the color of the menu item has to be reset to avoid having two items of the main menu being highlighted at the same time.
class CMenuBar : public CElement { private: //--- Switches the context menus of the main menu by hovering the cursor over it void SwitchContextMenuByFocus(const int active_item_index); //--- }; //+------------------------------------------------------------------+ //| Switches context menus of the main menu by hovering cursor over | //+------------------------------------------------------------------+ void CMenuBar::SwitchContextMenuByFocus(const int active_item_index) { int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Move to the following item menu if this one does not have a context menu if(::CheckPointer(m_contextmenus[i])==POINTER_INVALID) continue; //--- If you make it to the specified menu item, make its context menu visible if(i==active_item_index) m_contextmenus[i].Show(); //--- Hide the rest of context menus else { CContextMenu *cm=m_contextmenus[i]; //--- Hide the context menus, which are open from other context menus. // Iterate over the items of the current context menus to find out if there are any. int cm_items_total=cm.ItemsTotal(); for(int c=0; c<cm_items_total; c++) { CMenuItem *mi=cm.ItemPointerByIndex(c); //--- Move to the following menu item if the pointer to this one is incorrect if(::CheckPointer(mi)==POINTER_INVALID) continue; //--- Move to the following menu item if this one does not contain a context menu if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU) continue; //--- If there is a context menu and it is activated if(mi.ContextMenuState()) { //--- Send a signal for closing all context menus, which are open from this one ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,""); break; } } //--- Hide the context menu of the main menu m_contextmenus[i].Hide(); //--- Reset the color of the menu item m_items[i].ResetColors(); } } }
Now, we only have to add newly created methods to the CHARTEVENT_MOUSE_MOVE event handler of the CMenuBar class to check the event of the mouse cursor movement:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(id==CHARTEVENT_MOUSE_MOVE) { //--- Leave, if the main menu has not been activated if(!m_menubar_state) return; //--- Get the index of the activated item of the main menu int active_item_index=ActiveItemIndex(); if(active_item_index==WRONG_VALUE) return; //--- Change the color if the focus has changed ChangeObjectsColor(); //--- Switch the context menu by the activated item of the main menu SwitchContextMenuByFocus(active_item_index); return; } }
Final Test of the Main Menu
Now, we can test everything that has been done in this article. Add several independent menu items to the form. Add one more internal context menu to the main menu and attach this to the second item of the third context menu as shown in the screenshot below.
Compile the files and load the EA on to the chart. To have the same result as in the screenshot below, you can load the files attached at the end of this article.
Fig. 5. General test of the main menu.
Files attached at the end of this article also contain an indicator for tests with a similar graphical interface as the EA in the above screenshot. There are also versions for tests in the MetaTrader 4 trading platform.
Conclusion
This is the last article of the second part of the series. It is vast but we have considered almost all constituent parts of the library for developing graphical interfaces. The current structure of the library can be presented as shown in the schematic below. The detailed description can be found in the last chapter of the first part.
Fig. 6. The library structure at the current stage of development.
If you made it to this point, the good news is that the most difficult part of the job has been done. In the first and second parts of this series have considered the most complex topics concerning the development of graphical interfaces. Following articles will be dedicated mainly to the creation of controls. The material will be significantly simpler and there will not be such a variety of classes.
You can find and download archives with the library files at the current stage of development, icons and files of the programs (the EA, indicators and the script) considered in this article for testing in the Metatrader 4 and Metatrader 5 terminals. If you have questions on using the material presented in those files, you can refer to the detailed description of the library development in one of the articles from the list below or ask your question in the comments of this article.
List of the articles (chapters) of the second part:
- 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/2207
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Dear admin,
I tested an example file that are attached at the end of the article.
When I clicked on the menubar/mainmenu. their contextmenu didnt show completely.
Please give me some advice
Thank you.