Download MetaTrader 5

Graphical Interfaces III: Simple and Multi-Functional Buttons (Chapter 1)

31 March 2016, 16:25
Anatoli Kazharski
0
6 342

Contents

Introduction

In the previous two parts of the series, we considered many topics concerning the development of the library structure for creating graphical interfaces and basic mechanisms for managing objects.

In the second part of this series, an example of creating a control and connecting it with the library engine was considered in detail. That example was rather difficult. Admittedly, the main menu and context menus are some of the most complex controls.

This article will be significantly simpler than the previous ones. Here, we are going to consider the button control.

Button is the simplest control in the graphical interface that a user can interact with. At the same time, there can be several implementation options. In this article, we are going to create three classes for creating buttons of different levels of complexity.

  • Simple button. The CSimpleButton class.
  • Icon button. The CIconButton class.
  • Split button. The CSplitButton class.

Adding to that, we will implement other three classes for creating groups of interconnected buttons.

  • Group of simple buttons. The CButtonsGroup class.
  • Group of icon buttons. The CIconButtonsGroup class.
  • Group of radio buttons. The CRadioButtons class.

We will also introduce additions for enriching functionality of the context menu with one more mode. The CWindow form class will receive one more field with a method which will allow to define exactly which control blocked the form at the moment of its activation. This will allow to create conditions when the form can be unblocked only by the control that blocked it.

We will not discuss the methods characteristic of all controls as they have been thoroughly studied in the previous articles. Such methods will be shown in the code only as a declaration in the class body.

 


Developing the Class for Creating a Simple Button

Let us begin with a simple button. We have already prepared a class for creating a primitive object of control of the CButton type in the Objects.mqh file. CChartObjectButton is its base class, which can be used for creating a graphical object of the OBJ_BUTTON type. The properties of this object already imply two states - on and off. Its graphical representation can also have two options, depending on whether the display of the object frame is enabled. In both modes, the color of the button is slightly darker when it is pressed than when it is not.

This object can be attached to the chart manually from the main menu: Insert -> Objects -> Graphical objects -> Button. Parameters can also be changed manually from the settings window of the graphical object:

Fig. 1. The settings window of the button graphical object.

Fig. 1. The settings window of the button graphical object.

 

Then, in the Controls folder containing other files with control classes of our library, create the SimpleButton.mqh file. In this file, create the CSimpleButton class with fields and methods standard for all controls, which have been considered in detail in the previous articles.

//+------------------------------------------------------------------+
//|                                                 SimpleButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Class for creating a simple button                               |
//+------------------------------------------------------------------+
class CSimpleButton : public CElement
  {
private:
   //--- Pointer to the form to which the element is attached 
   CWindow          *m_wnd;
   //---
public:
                     CSimpleButton(void);
                    ~CSimpleButton(void);
   //--- Stores the form pointer
   void              WindowPointer(CWindow &object)          { m_wnd=::GetPointer(object);     }
   //---
public:
   //--- Chart event handler
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Timer
   virtual void      OnEventTimer(void);
   //--- Moving the element
   virtual void      Moving(const int x,const int y);
   //--- (1) Show, (2) hide, (3) reset, (4) delete
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Setting, (2) resetting priorities for the left mouse click
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };

Let us define what properties can be set up before creating the button

  • Size.
  • Background colors for different states and in relation to the mouse cursor location.
  • Frame colors for different states.
  • Text colors.

Sometimes, a button is required that automatically returns to its initial (unpressed) state after it was pressed. It also happens that the button is to stay pressed after it was unpressed and become unpressed if it was pressed. We will arrange that a button can be worked with in two modes by the user's choice. The IsPressed() method will be necessary for identifying if the button is currently pressed or unpressed.

Also, a possibility to block/unblock a button is required in case the developer of the application needs to use it. For instance, a button will be blocked if the conditions for using the function inbuilt in the button are not met. The button will become available as soon as conditions are met. We will consider such examples further in the article.

Let us add the methods for creating a button to the class. They are not different in principle to what has been already considered and you can find the code of those methods in the files attached to this article.

class CSimpleButton : public CElement
  {
private:
   //--- Object for creating a button
   CButton           m_button;
   //--- Button properties:
   //    (1) Text, (2) size
   string            m_button_text;
   int               m_button_x_size;
   int               m_button_y_size;
   //--- Background color
   color             m_back_color;
   color             m_back_color_off;
   color             m_back_color_hover;
   color             m_back_color_pressed;
   color             m_back_color_array[];
   //--- Frame color
   color             m_border_color;
   color             m_border_color_off;
   //--- Text color
   color             m_text_color;
   color             m_text_color_off;
   color             m_text_color_pressed;
   //--- Priority for left mouse click
   int               m_button_zorder;
   //--- Mode of two button states
   bool              m_two_state;
   //--- Available/blocked
   bool              m_button_state;
   //---
public:
   //--- Methods for creating a simple button
   bool              CreateSimpleButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   //---
public:
   //--- (1) setting up the button mode,
   //    (2) general state of the button (available/blocked)
   void              TwoState(const bool flag)               { m_two_state=flag;               }
   bool              IsPressed(void)                   const { return(m_button.State());       }
   bool              ButtonState(void)                 const { return(m_button_state);         }
   void              ButtonState(const bool state);
   //--- Button size
   void              ButtonXSize(const int x_size)           { m_button_x_size=x_size;         }
   void              ButtonYSize(const int y_size)           { m_button_y_size=y_size;         }
   //--- (1) Returns the button text, (2) setting up the color of the button text
   string            Text(void)                        const { return(m_button.Description()); }
   void              TextColor(const color clr)              { m_text_color=clr;               }
   void              TextColorOff(const color clr)           { m_text_color_off=clr;           }
   void              TextColorPressed(const color clr)       { m_text_color_pressed=clr;       }
   //--- Setting up the color of the button background
   void              BackColor(const color clr)              { m_back_color=clr;               }
   void              BackColorOff(const color clr)           { m_back_color_off=clr;           }
   void              BackColorHover(const color clr)         { m_back_color_hover=clr;         }
   void              BackColorPressed(const color clr)       { m_back_color_pressed=clr;       }
   //--- Setting up the color of the button frame
   void              BorderColor(const color clr)            { m_border_color=clr;             }
   void              BorderColorOff(const color clr)         { m_border_color_off=clr;         }
   //---
  };
//+------------------------------------------------------------------+
//| Changing the button state                                        |
//+------------------------------------------------------------------+
void CSimpleButton::ButtonState(const bool state)
  {
   m_button_state=state;
   m_button.State(false);
   m_button.Color((state)? m_text_color : m_text_color_off);
   m_button.BackColor((state)? m_back_color : m_back_color_off);
   m_button.BorderColor((state)? m_border_color : m_border_color_off);
  }

We are going to consider the event handling logic when a button is pressed. Add one more identifier ON_CLICK_BUTTON to the Defines.mqh file for generating a custom event. It will be used in all classes dedicated to creating a button.

#define ON_CLICK_BUTTON           (8)  // Pressing the button

Now, create the CSimpleButton::OnClickButton() method for handling the pressing of the button. Two checks are required at the beginning of the method: one for the name of the pressed object and one for the current state of the object. Negative result of the checks will lead to leaving the method. If the checks are passed, then handling is carried out considering what mode the button is in. If it becomes unpressed by itself, then it returns to its initial state and corresponding color. For the mode with two states, each state has two groups of colors. At the end of the method, a messaged is sent with the ON_CLICK_BUTTON identifier, the element identifier, the element index and the button name. 

class CSimpleButton : public CElement
  {
private:
   //--- Handling the pressing of a button
   bool              OnClickButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Event handling                                                   |
//+------------------------------------------------------------------+
void CSimpleButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling the event of the left mouse click on the object
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickButton(sparam))
         return;
     }
  }
//+------------------------------------------------------------------+
//| Handling the pressing of a button                                |
//+------------------------------------------------------------------+
bool CSimpleButton::OnClickButton(const string clicked_object)
  {
//--- Check for the object name
   if(m_button.Name()!=clicked_object)
      return(false);
//--- If the button is blocked
   if(!m_button_state)
     {
      m_button.State(false);
      return(false);
     }
//--- If the button mode has one state
   if(!m_two_state)
     {
      m_button.State(false);
      m_button.Color(m_text_color);
      m_button.BackColor(m_back_color);
     }
//--- If the button mode has two states
   else
     {
      //--- If the button is pressed
      if(m_button.State())
        {
         //--- Change the button color 
         m_button.State(true);
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         CElement::InitColorArray(m_back_color_pressed,m_back_color_pressed,m_back_color_array);
        }
      //--- If the button is unpressed
      else
        {
         //--- Change the button color 
         m_button.State(false);
         m_button.Color(m_text_color);
         m_button.BackColor(m_back_color);
         CElement::InitColorArray(m_back_color,m_back_color_hover,m_back_color_array);
        }
     }
//--- Emit an event
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_button.Description());
   return(true);
  }

We can already test the setting up of buttons on the chart with attachment to the form and receiving messages in the handler of the custom class of the application. Let us copy the test EA from the previous article. Leave only the main menu there with context menus attached to its items. The file with the CSimpleButton class must be included in the WndContainer.mqh file so it can be used in the custom class. 

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

After that, instances of the CSimpleButton class and methods for creating buttons can be declared in the CProgram custom class of the application. Let us create three buttons as an example. Two of them will unpress themselves after they are pressed and the third one will have an option to be fixed, that is will have two states (pressed/unpressed).

class CProgram : public CWndEvents
  {
private:
   //--- Simple buttons
   CSimpleButton     m_simple_button1;
   CSimpleButton     m_simple_button2;
   CSimpleButton     m_simple_button3;
   //---
private:
#define BUTTON1_GAP_X            (7)
#define BUTTON1_GAP_Y            (50)
   bool              CreateSimpleButton1(const string text);
#define BUTTON2_GAP_X            (128)
#define BUTTON2_GAP_Y            (50)
   bool              CreateSimpleButton2(const string text);
#define BUTTON3_GAP_X            (7)
#define BUTTON3_GAP_Y            (75)
   bool              CreateSimpleButton3(const string text);
  };

We will consider the code only for one of them. Please note the highlighted line of code where the mode of the button having two states is enabled. 

//+------------------------------------------------------------------+
//| Creates simple button 3                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateSimpleButton3(string button_text)
  {
//--- Pass the panel object
   m_simple_button3.WindowPointer(m_window);
//--- Coordinates
   int x=m_window.X()+BUTTON3_GAP_X;
   int y=m_window.Y()+BUTTON3_GAP_Y;
//--- Set up properties before creation
   m_simple_button3.TwoState(true);
   m_simple_button3.ButtonXSize(237);
   m_simple_button3.TextColor(clrBlack);
   m_simple_button3.TextColorPressed(clrBlack);
   m_simple_button3.BackColor(clrLightGray);
   m_simple_button3.BackColorHover(C'193,218,255');
   m_simple_button3.BackColorPressed(C'153,178,215');
//--- Creation of a button
   if(!m_simple_button3.CreateSimpleButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Add the object to the common array of object groups
   CWndContainer::AddToElementsArray(0,m_simple_button3);
   return(true);
  }

All the method for creating elements of graphical interface are called in the CProgram::CreateTradePanel() method:

//+------------------------------------------------------------------+
//| Creates the trading panel                                        |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creating a form for controls
//--- Creating controls:
//    Main menu
//--- Context menus
//--- Simple buttons
   if(!CreateSimpleButton1("Simple Button 1"))
      return(false);
   if(!CreateSimpleButton2("Simple Button 2"))
      return(false);
   if(!CreateSimpleButton3("Simple Button 3"))
      return(false);
//--- Redrawing of the chart
   m_chart.Redraw();
   return(true);
  }

Message with the ON_CLICK_BUTTON event identifier will be received in the CProgram::OnEvent() event handler as shown in the code below. As an example, we will implement the block of code where the button name will be checked. If it turns out that the third button was pressed, its current state will define the state of the second button. That is, if the third button is pressed, the second one will be blocked and vice versa.

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Pressing the button event
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(sparam==m_simple_button3.Text())
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
     }
  }

The screenshots below show the result of the compilation and attaching the program to the chart. In the left screenshot, the Simple Button 3 button is unpressed and the Simple Button 1 button is available. In the right screenshot, the Simple Button 3 button is pressed and the Simple Button 1 button is blocked. 

 Fig. 2. Test of attaching the button control to the chart. Buttons in different states.

Fig. 2. Test of attaching the button control to the chart. Buttons in different states. 

 

The current implementation is missing one nuance which would add realism to the interaction with the button. We need to make a button change the color straight after it was pressed. A check if the left mouse button is pressed can be carried out by the CHARTEVENT_MOUSE_MOVE cursor movement event, therefore add correspondent code to the CSimpleButton::OnEvent() event handler. 

In cases when (1) the element is hidden, (2) the form is blocked, (3) left mouse button is unclicked (4) and the button is blocked, the program leaves the handler. If all these checks are passed, then the relevant color is set up depending on the focus and the current state of the button.

//+------------------------------------------------------------------+
//| Event handling                                                   |
//+------------------------------------------------------------------+
void CSimpleButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the cursor movement event
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      if(!CElement::IsVisible())
         return;
      //--- Identify the focus
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Leave, if the form is blocked
      if(m_wnd.IsLocked())
         return;
      //--- Leave, if the mouse button is unpressed
      if(sparam=="0")
         return;
      //--- Leave, if the button is blocked
      if(!m_button_state)
         return;
      //--- If there is no focus
      if(!CElement::MouseFocus())
        {
         //--- If the button is unpressed
         if(!m_button.State())
            {
             m_button.Color(m_text_color);
             m_button.BackColor(m_back_color);
            }
         //---
         return;
        }
      //--- If there is focus
      else
        {
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         return;
        }
      //---
      return;
     }
  }

Now, everything will work as designed. The development of the class for creating a simple button is finished. You can see its detailed code in the files attached to the article. Now, we are going to consider the class for a button with extended functionality.

 


Developing the Class for Creating an Icon Button

An icon button will be composed of three graphical primitive objects:

  1. Background.
  2. Icon.
  3. Text label.

Fig. 3. Compound parts of the icon button control.

Fig. 3. Compound parts of the icon button control.

 

A text label is required for a free positioning of the button text. For instance, the button can be composed in the way that its text is in the bottom part and its icon is in the upper part and vice versa. We will consider such articled further in the article.

The class for creating this control will contain the same fields and methods as the CSimpleButton class for creating a simple button. Besides the properties that concern to the size of the button and its color, we will require fields and methods for setting up margins for the icon and text label in relation to the control coordinates as well as the label icons in the active and blocked state. Let us also add an option to create a button with an icon only. Such a button will be composed of only one object of the OBJ_BITMAP_LABEL type.

Then, create the IconButton.mqh file and the CIconButton class in it. Include it in the WndContainer.mqh file in the same way as other controls. The code below shows only the fields and methods of the CIconButton class that make it different from the CSimpleButton class for creating a simple button.

class CIconButton : public CElement
  {
private:
   //--- Objects for creating a button
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   //--- Button properties:
   //    Icons for the button in both active and blocked states
   string            m_icon_file_on;
   string            m_icon_file_off;
   //--- Icon margins
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //--- Text and margins of the text label
   string            m_label_text;
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- The icon only mode if the button is composed only of the BmpLabel object
   bool              m_only_icon;
   //---
public:
   //--- Methods for creating a button
   bool              CreateIconButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   //---
public:
   //--- Setting up the icon only mode
   void              OnlyIcon(const bool flag)                { m_only_icon=flag;               }
   //--- Setting up icons for the button in the active and blocked states
   void              IconFileOn(const string file_path)       { m_icon_file_on=file_path;       }
   void              IconFileOff(const string file_path)      { m_icon_file_off=file_path;      }
   //--- Icon margins
   void              IconXGap(const int x_gap)                { m_icon_x_gap=x_gap;             }
   void              IconYGap(const int y_gap)                { m_icon_y_gap=y_gap;             }
   //--- Text label margins
   void              LabelXGap(const int x_gap)               { m_label_x_gap=x_gap;            }
   void              LabelYGap(const int y_gap)               { m_label_y_gap=y_gap;            }
  };

All class fields must be initialized with default values:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIconButton::CIconButton(void) : m_icon_x_gap(4),
                                 m_icon_y_gap(3),
                                 m_label_x_gap(25),
                                 m_label_y_gap(4),
                                 m_icon_file_on(""),
                                 m_icon_file_off(""),
                                 m_button_state(true),
                                 m_two_state(false),
                                 m_only_icon(false),
                                 m_button_y_size(18),
                                 m_back_color(clrLightGray),
                                 m_back_color_off(clrLightGray),
                                 m_back_color_hover(clrSilver),
                                 m_back_color_pressed(clrBlack),
                                 m_border_color(clrWhite),
                                 m_border_color_off(clrDarkGray),
                                 m_label_color(clrBlack),
                                 m_label_color_off(clrDarkGray),
                                 m_label_color_hover(clrBlack),
                                 m_label_color_pressed(clrBlack)
  {
//--- Store the name of the element class in the base class  
   CElement::ClassName(CLASS_NAME);
//--- Set up priorities for the left mouse button click
   m_button_zorder =1;
   m_zorder        =0;
  }

Methods for creating all button objects will contain a check for the icon only mode. If it is established that the button is to be composed only of an icon, then the program will leave at the very beginning of the method for creating the background and the text label.

//+------------------------------------------------------------------+
//| Creates the button background                                    |
//+------------------------------------------------------------------+
bool CIconButton::CreateButton(void)
  {
//--- Leave, if the icon only mode is enabled
   if(m_only_icon)
      return(true);
//--- ... etc.
  }
//+------------------------------------------------------------------+
//| Creates the button text                                          |
//+------------------------------------------------------------------+
bool CIconButton::CreateLabel(void)
  {
//--- Leave, if the icon only mode is enabled
   if(m_only_icon)
      return(true);
//--- ... etc.
  }

When a button is to be composed only of an icon, the method for creating the latter will contain one more check for presence of icons. If an icon is not defined by the user, then the creation of the graphical interface will be finished at this stage and the journal will receive a message that presence of icons in this mode is compulsory. The CIconButton::CreateIcon() method for creating a button icon is presented in the code below:

//+------------------------------------------------------------------+
//| Creates a button icon                                            |
//+------------------------------------------------------------------+
bool CIconButton::CreateIcon(void)
  {
//--- If the icon only mode disabled
   if(!m_only_icon)
     {
      //--- Leave, if the icon for the button is not required
      if(m_icon_file_on=="" || m_icon_file_off=="")
         return(true);
     }
//--- If the icon only mode is enabled 
   else
     {
      //--- If the icon has not been defined, print the message and leave
      if(m_icon_file_on=="" || m_icon_file_off=="")
        {
         ::Print(__FUNCTION__," > The icon must be defined in the \"Icon only\" mode.");
         return(false);
        }
     }
//--- Forming of the object name
   string name=CElement::ProgramName()+"_icon_button_bmp_"+(string)CElement::Id();
//--- Coordinates
   int x =(!m_only_icon)? m_x+m_icon_x_gap : m_x;
   int y =(!m_only_icon)? m_y+m_icon_y_gap : m_y;
//--- Set up the icon
   if(!m_icon.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Set up properties
   m_icon.BmpFileOn("::"+m_icon_file_on);
   m_icon.BmpFileOff("::"+m_icon_file_off);
   m_icon.State(true);
   m_icon.Corner(m_corner);
   m_icon.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_icon.Selectable(false);
   m_icon.Z_Order((!m_only_icon)? m_zorder : m_button_zorder);
   m_icon.Tooltip((!m_only_icon)? "\n" : m_label_text);
//--- Store coordinates
   m_icon.X(x);
   m_icon.Y(y);
//--- Store the size
   m_icon.XSize(m_icon.X_Size());
   m_icon.YSize(m_icon.Y_Size());
//--- Margins from the edge
   m_icon.XGap(x-m_wnd.X());
   m_icon.YGap(y-m_wnd.Y());
//--- Store the object pointer
   CElement::AddToArray(m_icon);
   return(true);
  }

The CIconButton class does not have any other differences from what we considered in the CSimpleButton class. You can find the full version in the files attached to this article.

Now, we are going to test buttons of the CIconButton type. To demonstrate the capability, create five such buttons with different modes, sizes and states in the test EA. Create five instances of the CIconButton class in the custom class of the test EA and declare five methods for creating buttons as shown in the code below. 

class CProgram : public CWndEvents
  {
private:
   //--- Icon buttons
   CIconButton       m_icon_button1;
   CIconButton       m_icon_button2;
   CIconButton       m_icon_button3;
   CIconButton       m_icon_button4;
   CIconButton       m_icon_button5;
   //---
private:
   //--- Icon buttons
#define ICONBUTTON1_GAP_X        (7)
#define ICONBUTTON1_GAP_Y        (105)
   bool              CreateIconButton1(const string text);
#define ICONBUTTON2_GAP_X        (128)
#define ICONBUTTON2_GAP_Y        (105)
   bool              CreateIconButton2(const string text);
#define ICONBUTTON3_GAP_X        (7)
#define ICONBUTTON3_GAP_Y        (130)
   bool              CreateIconButton3(const string text);
#define ICONBUTTON4_GAP_X        (88)
#define ICONBUTTON4_GAP_Y        (130)
   bool              CreateIconButton4(const string text);
#define ICONBUTTON5_GAP_X        (169)
#define ICONBUTTON5_GAP_Y        (130)
   bool              CreateIconButton5(const string text);
  };

We will use the implementation of only one of these methods as an example because all of them are the same with the only exception of the parameter values of the button being set:

//+------------------------------------------------------------------+
//| Creates icon button 5                                            |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp"
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp"
//---
bool CProgram::CreateIconButton5(const string button_text)
  {
//--- Pass the panel object
   m_icon_button5.WindowPointer(m_window);
//--- Coordinates
   int x=m_window.X()+ICONBUTTON5_GAP_X;
   int y=m_window.Y()+ICONBUTTON5_GAP_Y;
//--- Set up properties before creation
   m_icon_button5.ButtonXSize(76);
   m_icon_button5.ButtonYSize(87);
   m_icon_button5.LabelXGap(6);
   m_icon_button5.LabelYGap(69);
   m_icon_button5.LabelColor(clrBlack);
   m_icon_button5.LabelColorPressed(clrBlack);
   m_icon_button5.BackColor(clrGainsboro);
   m_icon_button5.BackColorHover(C'193,218,255');
   m_icon_button5.BackColorPressed(C'210,210,220');
   m_icon_button5.BorderColor(C'150,170,180');
   m_icon_button5.BorderColorOff(C'178,195,207');
   m_icon_button5.IconXGap(6);
   m_icon_button5.IconYGap(3);
   m_icon_button5.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp");
   m_icon_button5.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp");
//--- Create control
   if(!m_icon_button5.CreateIconButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Add the control pointer to the base
   CWndContainer::AddToElementsArray(0,m_icon_button5);
   return(true);
  }

 Place the calls of these methods in the main method for creating the graphical interface of the program. 

Add tracking of the pressing Icon Button 2 to the event handler. In the EA version, which is attached at the end of this article, this button works in two modes (pressed/unpressed). The availability of Icon Button 1 and Icon Button 4 depends on the state of Icon Button 2. If this button is unpressed, then all the buttons dependent on it are blocked and vice versa. 

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Pressing the button event
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(sparam==m_simple_button3.Text())
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
      //---
      if(sparam==m_icon_button2.Text())
        {
         if(m_icon_button2.IsPressed())
           {
            m_icon_button1.ButtonState(true);
            m_icon_button4.ButtonState(true);
           }
         else
           {
            m_icon_button1.ButtonState(false);
            m_icon_button4.ButtonState(false);
           }
        }
     }
  }

After compiling the files and loading the test EA on to the chart, you should see the result as shown in the screenshot below:

Fig. 4. Test of the icon button control.

Fig. 4. Test of the icon button control.

 

We have completed the development of the CIconButton class for creating a button with extended functionality. The icons in the buttons that you see in the screenshot above can be downloaded at the end of the article. Now, we will discuss the class for creating a split button. 


Developing the Class for Creating a Split Button

What is a split button? A split button is a button composed of two functioning parts:

  • The first part is a button with the inbuilt main function.
  • The second part is a button that brings up a context menu with additional functions.

This type of control is frequently used in graphical interfaces of many programs. Such buttons are used when several functions must be densely grouped and all of them belong to the same category. 

A split button will be composed of five objects (graphical primitives) and one attachable element (context menu):

  1. Background.
  2. Icon.
  3. Text.
  4. Background of the additional button.
  5. Drop-down menu indicator.

 

Fig. 5. Compound parts of the split button control.

Fig. 5. Compound parts of the split button control.

 

We can see that the context menu (object of the CContextMenu class) will not be attached to the object of the CMenuItem class. This means that we require an additional mode when a context menu can be a part of any other element or even be detached. 

Besides, we also need a reference point for a quality setting up of the interaction between those controls which are enabled by the user temporarily (drop-down elements). It must be arranged that the form can be unblocked only by the element that blocked it. If this is not done, the elements may conflict with each other as the state of the form can define the state of some other controls. We will illustrate this with an example later. A test can be carried out for better understanding in what situations exactly such conflicts can occur. To implement this functionality, a field and a method for storing and getting an activated elements have to be added to the CWindow class of the form as shown in the code below.

class CWindow : public CElement
  {
private:
   //--- Identifier of the activated control
   int               m_id_activated_element;
   //---
public:
   //--- Methods for storing and getting the id of the activated element
   int               IdActivatedElement(void)                          const { return(m_id_activated_element);     }
   void              IdActivatedElement(const int id)                        { m_id_activated_element=id;          }
  };

If during the element activation it is blocking the form, the identifier of this element is to be stored in the form. At the location where the form is unblocked, a check must be carried out for the identifier of the element that blocked it. The check for the identifier of the element that blocked the form is not required where there is a check for the name of the object pressed on before unblocking the form. 

Then, we are going to introduce additions to the CContextMenu class. They will allow the enabling and handling of the detached context menu mode as shown in the code below. The mode of the context menu with the attachment to the previous node is set by default: 

class CContextMenu : public CElement
  {
   //--- The detached context menu mode. This means that there is no attachment to the previous node.
   bool              m_free_context_menu;
   //---
public:
   //--- Setting the detached context menu mode
   void              FreeContextMenu(const bool flag)               { m_free_context_menu=flag;             }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_free_context_menu(false)
  {
  }

Internal identifiers for handling of the pressing on the menu item event from the attached and detached context menu will differ. Such a division will make the code unambiguous, decrease the number of conditions and allow managing events of the elements in different modes in a more flexible way.

Add a new identifier (ON_CLICK_FREEMENU_ITEM) for generating an event from a detached context menu to the Defines.mqh file:

#define ON_CLICK_FREEMENU_ITEM    (9)  // Clicking on the item of a detached context menu

Additional conditions with the check of the detached context menu mode must be added in the next places of the CContextMenu class. Below are shortened versions of the methods. The comments are left for orientation.

1. In the event handler:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the mouse cursor movement
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      //--- Get the focus
      //--- Leave, if this is a detached context menu
      if(m_free_context_menu)
         return;
      //--- If the context menu is enabled and the left mouse button is pressed
      //--- Check the conditions for closing all context menus which were open below this one
      return;
     }
//--- Handling the event of the left mouse click on the object
//--- Handling the ON_CLICK_MENU_ITEM event
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
     {
      //--- Leave, if this is a detached context menu
      if(m_free_context_menu)
         return;
      //--- Receiving the message from the menu item for handling
      return;
     }
  }

2. In the method for creating a context menu:

//+------------------------------------------------------------------+
//| Creates a context menu                                           |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Leave, if there is no form pointer
//--- If this is an attached context menu
   if(!m_free_context_menu)
     {
      //--- Leave, if there is no pointer to the previous node 
      if(::CheckPointer(m_prev_node)==POINTER_INVALID)
        {
         ::Print(__FUNCTION__," > Before creating a context menu it must be passed "
                 "the pointer to the previous node using the CContextMenu::PrevNodePointer(CMenuItem &object) method.");
         return(false);
        }
     }
//--- Initialization of variables
//--- If coordinates have not been specified
//--- If coordinates have been specified
//--- Margins from the edge
//--- Creating a context menu
//--- Hide the element
   return(true);
  }

3. In the method for creating a list of menu items:

//+------------------------------------------------------------------+
//| Creates a list of menu items                                     |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // For identifying the location of separation lines
   int x =m_x+1; // X coordinate
   int y =m_y+1; // Y coordinate. Will be calculated in a loop for every menu item.
//--- Number of separation lines
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Calculation of the Y coordinate
      //--- Store the form pointer
      //--- If the context menu has an attachment, add the pointer to the previous node
      if(!m_free_context_menu)
         m_items[i].PrevNodePointer(m_prev_node);
      //--- Set up properties
      //--- Margins from the edge of the panel
      //--- Creating a menu item
      //--- Move to the following one if all separation lines have been set
      //--- If indices match, set a separation line after this menu item
     }
   return(true);
  }

4. In the methods for showing and hiding a context menu: 

//+------------------------------------------------------------------+
//| Shows a context menu                                             |
//+------------------------------------------------------------------+
void CContextMenu::Show(void)
  {
//--- Leave, if this control is already visible
//--- Show the objects of the context menu
//--- Show the menu items
//--- Assign the status of a visible control
//--- State of the context menu
//--- Register the state in the previous node
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(true);
//--- Block the form
  }
//+------------------------------------------------------------------+
//| Hides a context menu                                             |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Leave, if the element is hidden
//--- Hide the objects of the context menu
//--- Hide menu items
//--- Zero the focus
//--- Assign the status of a hidden element
//--- State of the context menu
//--- Register the state in the previous node
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(false);
  }

5. In the method for the handling the pressing of the menu item. In the new block, a check is carried out in a loop for the name of the pressed object in the detached context menu mode. If such an object is found, then an event is emitted with the ON_CLICK_FREEMENU_ITEM identifier. Later, this event will have to be tracked in the event handlers of those controls that contain a context menu (this will be illustrated with the example of a split button). 

//+------------------------------------------------------------------+
//| Handling the pressing of a menu item                             |
//+------------------------------------------------------------------+
bool CContextMenu::OnClickMenuItem(const string clicked_object)
  {
//--- Leave, if this context menu has a previous node and is already open
   if(!m_free_context_menu && m_context_menu_state)
      return(true);
//--- Leave, if the pressing was not on the menu item
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0)
      return(false);
//--- Get the identifier and index from the object name
   int id    =IdFromObjectName(clicked_object);
   int index =IndexFromObjectName(clicked_object);
//--- If the context menu has a previous node
   if(!m_free_context_menu)
     {
      //--- Leave, if the pressing was not on the 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();
     }
//--- If this is a detached context menu
   else
     {
      //--- Find in a loop the menu item which was pressed
      int total=ItemsTotal();
      for(int i=0; i<total; i++)
        {
         if(m_items[i].Object(0).Name()!=clicked_object)
            continue;
         //--- Send a message about it
         ::EventChartCustom(m_chart_id,ON_CLICK_FREEMENU_ITEM,CElement::Id(),i,DescriptionByIndex(i));
         break;
        }
     }
//---
   return(true);
  }

Everything is ready for developing the class of a split button. Create the SplitButton.mqh file with the CSplitButton class and methods standard for all controls in the Controls folder:

//+------------------------------------------------------------------+
//|                                                  SplitButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "ContextMenu.mqh"
//+------------------------------------------------------------------+
//| Class for creating a split button                                |
//+------------------------------------------------------------------+
class CSplitButton : public CElement
  {
private:
   //--- Pointer to the form to which the element is attached
   CWindow          *m_wnd;
   //---
public:
                     CSplitButton();
                    ~CSplitButton();

   //--- 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);
   //--- Timer
   virtual void      OnEventTimer(void);
   //--- Moving the element
   virtual void      Moving(const int x,const int y);
   //--- (1) Show, (2) hide, (3) reset, (4) delete
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Setting, (2) resetting priorities for the left mouse click
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };

In addition to the properties and methods characteristic for the buttons of all types that have already been described in this article, we need additional ones for setting up buttons with a drop-down menu:

  • Size. In this version we will use only width. The height will be equal to the height of the main button.
  • Priority for the left mouse button click. A button with a drop-down menu must have a higher priority than a simple button.
  • Icons and margins for the button icon.
  • State of the context menu of the button (visible/hidden). 

class CSplitButton : public CElement
  {
private:
   //--- Size and priority of the left mouse click for the button with a drop-down menu
   int               m_drop_button_x_size;
   int               m_drop_button_zorder;
   //--- Icon margins
   int               m_drop_arrow_x_gap;
   int               m_drop_arrow_y_gap;
   //--- Icons of the button with a drop-down menu in the active and blocked states
   string            m_drop_arrow_file_on;
   string            m_drop_arrow_file_off;
   //--- State of the context menu 
   bool              m_drop_menu_state;
   //---
public:
   //--- Size of the button with a drop-down menu
   void              DropButtonXSize(const int x_size)        { m_drop_button_x_size=x_size;        }
   //--- Setting up icons for a button with a drop-down menu in the active and blocked states
   void              DropArrowFileOn(const string file_path)  { m_drop_arrow_file_on=file_path;     }
   void              DropArrowFileOff(const string file_path) { m_drop_arrow_file_off=file_path;    }
   //--- Icon margins
   void              DropArrowXGap(const int x_gap)           { m_drop_arrow_x_gap=x_gap;           }
   void              DropArrowYGap(const int y_gap)           { m_drop_arrow_y_gap=y_gap;           }
  };

As mentioned before, creating a split button requires five primitive objects and a context menu. Let us declare instances of required classes and methods for their creation. We will also need methods for forming a context menu (adding menu items and separation lines). Since the properties of the context menu are set up by the user, a method for getting the pointer to the button context menu is required.

class CSplitButton : public CElement
  {
private:
   //--- Objects for creating a button
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CEdit             m_drop_button;
   CBmpLabel         m_drop_arrow;
   CContextMenu      m_drop_menu;
   //---
public:
   //--- Methods for creating a button
   bool              CreateSplitButton(const long chart_id,const string button_text,const int window,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateDropButton(void);
   bool              CreateDropIcon(void);
   bool              CreateDropMenu(void);
   //---
public:
   //--- Getting the context menu pointer,
   CContextMenu     *GetContextMenuPointer(void)        const { return(::GetPointer(m_drop_menu));  }
   //--- Adds a menu item with specified properties before the creation of the context menu
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off);
   //--- Adds a separation line after the specified menu item before the creation of the context menu
   void              AddSeparateLine(const int item_index);
  };

Methods for creating element objects do not have principal differences from the ones that have been considered before. The only important nuance here is that in the method for creating a context menu the identifier of the split button must be set up as a part of the control as shown in the code below. Later, this will allow to establish by the identifier what menu item the ON_CLICK_FREEMENU_ITEM message came from.

//+------------------------------------------------------------------+
//| Creates a drop-down menu                                         |
//+------------------------------------------------------------------+
bool CSplitButton::CreateDropMenu(void)
  {
//--- Pass the panel object
   m_drop_menu.WindowPointer(m_wnd);
//--- Detached context menu
   m_drop_menu.FreeContextMenu(true);
//--- Coordinates
   int x=m_x;
   int y=m_y+m_y_size;
//--- Set up properties
   m_drop_menu.Id(CElement::Id());
   m_drop_menu.XSize((m_drop_menu.XSize()>0)? m_drop_menu.XSize() : m_button_x_size);
//--- Set up a context menu
   if(!m_drop_menu.CreateContextMenu(m_chart_id,m_subwin,x,y))
      return(false);
//---
   return(true);
  }

We are going to consider methods for interaction with a split button. Since a button of this type has two parts, two separate methods are required for handling the pressing of both of them. These methods will be called in the class handler in the block of the CHARTEVENT_OBJECT_CLICK event

class CSplitButton : public CElement
  {
private:
   //--- Handling the pressing of a button
   bool              OnClickButton(const string clicked_object);
   //--- Handling the pressing of the button with a drop down menu
   bool              OnClickDropButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling the event of the left mouse click on the object
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Pressing of the simple button
      if(OnClickButton(sparam))
         return;
      //--- Pressing of the button with a drop-down menu
      if(OnClickDropButton(sparam))
         return;
     }
  }

In the CSplitButton::OnClickButton() method for handling the pressing of the main button, the object name is checked at first. If this is the object name of this class instance, then the button state is checked. If the button is blocked, then the program leaves the method. The main button can be only in one state. This means that it must return to the unpressed state after it was pressed. If all the checks are passed, then (1) the context menu must be hidden if it is visible, (2) the corresponding status of the state and color for menu and button must be set, (3) the form must be unblocked and the identifier of the activating element must be zeroed in its memory. 

At the end of the method a message is to be sent. It can be received in the custom class. This message will contain (1) the ON_CLICK_BUTTON event identifier, (2) the element identifier, (3) the element index and (4) the displayed description of the button. 

//+------------------------------------------------------------------+
//| Pressing the button                                              |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickButton(const string clicked_object)
  {
//--- Leave, if the name of the object does not match  
   if(clicked_object!=m_button.Name())
      return(false);
//--- Leave, if the button is blocked
   if(!m_button_state)
     {
      //--- Unpress the button
      m_button.State(false);
      return(false);
     }
//--- Hide the menu
   m_drop_menu.Hide();
   m_drop_menu_state=false;
//--- Unpress the button and set up the color of the focus
   m_button.State(false);
   m_button.BackColor(m_back_color_hover);
   m_drop_button.BackColor(m_back_color_hover);
//--- Unblock the form
   m_wnd.IsLocked(false);
   m_wnd.IdActivatedElement(WRONG_VALUE);
//--- Send a message about it
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_label.Description());
   return(true);
  }

The CSplitButton::OnClickDropButton() method, where handling the pressing of the button with a drop-down menu is carried out, also contains two checks at the very beginning. They are checks for (1) the name and (2) the button availability. Then, the program goes to one of two blocks of code depending on the current visibility state of the context menu button to hide or show it.

//+------------------------------------------------------------------+
//| Pressing of the button with a drop-down menu                     |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickDropButton(const string clicked_object)
  {
//--- Leave, if the name of the object does not match  
   if(clicked_object!=m_drop_button.Name())
      return(false);
//--- Leave, if the button is blocked
   if(!m_button_state)
     {
      //--- Unpress the button
      m_button.State(false);
      return(false);
     }
//--- If the list is shown, hide it
   if(m_drop_menu_state)
     {
      m_drop_menu_state=false;
      m_drop_menu.Hide();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_hover);
      //--- Unblock the form and zero the activating element id
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
//--- If the list is hidden, show it
   else
     {
      m_drop_menu_state=true;
      m_drop_menu.Show();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_pressed);
      //--- Block the form and store the activating element id
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
     }
//---
   return(true);
  }

Earlier in this article we enriched the CContextMenu class of the context menu with an addition, which in the free mode facilitates sending an event for the internal use with the ON_CLICK_FREEMENU_ITEM identifier. This message will be received in the handler of the CSplitButton class of a split button. To identify that the message was sent from a relative context menu, the element identifier must be checked. It is contained in the lparam parameter. If identifiers match, (1) the menu must be hidden, (2) colors corresponding to the button state must be set up and (3) the form must be unblocked if this element was the activator. After that, a message with the ON_CLICK_CONTEXTMENU_ITEM identifier is sent. This message can be received in the custom class.

In addition to that, we will create an additional method CSplitButton::HideDropDownMenu() for multiple use. The purpose of this method is to hide the menu and unblock the form with zeroing the identifier of the activating element.

class CSplitButton : public CElement
  {
private:
   //--- Hides the drop-down menu
   void              HideDropDownMenu(void);
  };
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling the event of pressing of the free menu item
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_FREEMENU_ITEM)
     {
      //--- Leave, if the identifiers do not match
      if(CElement::Id()!=lparam)
         return;
      //--- Hide the drop-down menu
      HideDropDownMenu();
      //--- Send a message
      ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,lparam,dparam,sparam);
      return;
     }
  }
//+------------------------------------------------------------------+
//| Hides a drop-down menu                                           |
//+------------------------------------------------------------------+
void CSplitButton::HideDropDownMenu(void)
  {
//--- Hide the menu and set up corresponding indications
   m_drop_menu.Hide();
   m_drop_menu_state=false;
   m_button.BackColor(m_back_color);
   m_drop_button.BackColor(m_back_color);
//--- Unblock the form if the identifiers of the form and this element match
   if(m_wnd.IdActivatedElement()==CElement::Id())
     {
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
  }

Now, we have to set up the reaction of the split button to the mouse cursor location and to the state of the left mouse button when the cursor is hovering over the button. That will require another method that will be called CSplitButton::CheckPressedOverButton(). This method has only one parameter - the state of the left mouse button. At the beginning there are two checks. If it turns out that (1) the cursor is outside of the button area and (2) the form is blocked when this is not the activating element, then the program will leave the method. If the checks are passed, then the program sets up relevant colors depending on the left mouse button state and on what part of the button the cursor is over.

class CSplitButton : public CElement
  {
private:
   //--- Check of the pressed left mouse button over a split button
   void              CheckPressedOverButton(const bool mouse_state);
  };
//+------------------------------------------------------------------+
//| Check of the pressed left mouse button over a split button       |
//+------------------------------------------------------------------+
void CSplitButton::CheckPressedOverButton(const bool mouse_state)
  {
//--- Leave, if it is outside of the element area
   if(!CElement::MouseFocus())
      return;
//--- Leave, if the form is blocked and the identifiers of the form and this element do not match
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      return;
//--- Mouse button pressed
   if(mouse_state)
     {
      //--- In the menu button area
      if(m_drop_button.MouseFocus())
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
      else
        {
         m_button.BackColor(m_back_color_pressed);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
//--- Mouse button unpressed
   else
     {
      if(m_drop_menu_state)
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
  }

The CSplitButton::CheckPressedOverButton() method is called in the handler by the mouse cursor movement event. Several checks precede calling this method such as (1) whether the element is hidden, (2) the focus, (3) if the element is available and also (4) if the cursor is outside of the element area, which all can cause the menu to be hidden and the handler to leave before calling the CSplitButton::CheckPressedOverButton() method. 

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the cursor movement event
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the element is hidden
      if(!CElement::IsVisible())
         return;
      //--- Identify the focus
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      m_drop_button.MouseFocus(x>m_drop_button.X() && x<m_drop_button.X2() && 
                               y>m_drop_button.Y() && y<m_drop_button.Y2());
      //--- Leave, if the button is blocked
      if(!m_button_state)
         return;
      //--- Outside of the element area and with pressed mouse button
      if(!CElement::MouseFocus() && sparam=="1")
        {
         //--- Leave, if the focus is in the context menu
         if(m_drop_menu.MouseFocus())
            return;
         //--- Hide the drop-down menu
         HideDropDownMenu();
         return;
        }
      //--- Check of the pressed left mouse button over a split button
      CheckPressedOverButton(bool((int)sparam));
      return;
     }
  }

The class of the split button control is ready for testing. For it to work correctly, it has to be embedded in the library structure properly. This must be done every time a complex (compound) control is created. In case of buttons of the CSimpleButton and CIconButton type no additions were required. It is different for the split button as besides the actual button, there is also a context menu which also must make it to the relevant private array in the base of control identifiers. The end user of the library will be using its final version and will not deal with the code. He will not have any idea how it works. The main goal of the library developer is to make using the library very simple, that is to ensure that creating the graphical interface of the program takes a minimal number of actions.

Include the file with the CSplitButton class to the WndContainer.mqh file: 

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

Then, declare and implement the method for adding the pointer to the context menu with a split button to the private array, which has been created previously:

//+------------------------------------------------------------------+
//| Class for storing all interface objects                          |
//+------------------------------------------------------------------+
class CWndContainer
  {
private:
   //--- Stores the pointers to the split button elements in the base
   bool              AddSplitButtonElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Stores the pointers to the elements of a split button in the base|
//+------------------------------------------------------------------+
bool CWndContainer::AddSplitButtonElements(const int window_index,CElement &object)
  {
//--- Leave, if this is not a split button
   if(object.ClassName()!="CSplitButton")
      return(false);
//--- Get the split button pointer
   CSplitButton *sb=::GetPointer(object);
//--- Increasing the element array
   int size=::ArraySize(m_wnd[window_index].m_elements);
   ::ArrayResize(m_wnd[window_index].m_elements,size+1);
//--- Getting the context menu pointer
   CContextMenu *cm=sb.GetContextMenuPointer();
//--- Store the element and objects in the base
   m_wnd[window_index].m_elements[size]=cm;
   AddToObjectsArray(window_index,cm);
//--- 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
      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 the pointers to all menu item objects 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);
  }

As you can remember, calling of the methods like CWndContainer::AddSplitButtonElements() must be carried out in theCWndContainer::AddToElementsArray() method as shown in the shortened version of this method in the code below.

//+------------------------------------------------------------------+
//| Adds the pointer to the element array                            |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- If the base does not contain any 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 the element identifiers
//--- Stores the pointers to the context menu objects in the base
//--- Stores the pointers to the main menu objects in the base
//--- Stores the pointers to the split button objects in the base
   if(AddSplitButtonElements(window_index,object))
      return;
  }

Everything is ready for testing split buttons. Create four such buttons in the test EA. Declare instances of the CSplitButton class and methods for creating buttons with margins from the top left point of the form. Place their call in the main method for creating the graphical interface of the program.

class CProgram : public CWndEvents
  {
private:
   //--- Split buttons
   CSplitButton      m_split_button1;
   CSplitButton      m_split_button2;
   CSplitButton      m_split_button3;
   CSplitButton      m_split_button4;
   //---
private:
   //--- Split buttons
#define SPLITBUTTON1_GAP_X       (7)
#define SPLITBUTTON1_GAP_Y       (225)
   bool              CreateSplitButton1(const string text);
#define SPLITBUTTON2_GAP_X       (128)
#define SPLITBUTTON2_GAP_Y       (225)
   bool              CreateSplitButton2(const string text);
#define SPLITBUTTON3_GAP_X       (7)
#define SPLITBUTTON3_GAP_Y       (250)
   bool              CreateSplitButton3(const string text);
#define SPLITBUTTON4_GAP_X       (128)
#define SPLITBUTTON4_GAP_Y       (250)
   bool              CreateSplitButton4(const string text);
  };
//+------------------------------------------------------------------+
//| Creates the trading panel                                        |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creating a form for controls
//--- Creating controls:
//    Main menu
//--- Context menus
//--- Simple buttons
//--- Icon buttons
//--- Split buttons
   if(!CreateSplitButton1("Split Button 1"))
      return(false);
   if(!CreateSplitButton2("Split Button 2"))
      return(false);
   if(!CreateSplitButton3("Split Button 3"))
      return(false);
   if(!CreateSplitButton4("Split Button 4"))
      return(false);
//--- Redrawing of the chart
   m_chart.Redraw();
   return(true);
  }

We will use one of them as an example and show it in the code below. Please note that to set up properties of the context menu, at first its pointer has to be obtained using the CSplitButton::GetContextMenuPointer() method.

//+------------------------------------------------------------------+
//| Creates split button 1                                           |
//+------------------------------------------------------------------+
bool CProgram::CreateSplitButton1(const string button_text)
  {
//--- Three items in the context menu
#define CONTEXTMENU_ITEMS5 3
//--- Pass the panel object
   m_split_button1.WindowPointer(m_window);
//--- Coordinates
   int x=m_window.X()+SPLITBUTTON1_GAP_X;
   int y=m_window.Y()+SPLITBUTTON1_GAP_Y;
//--- Array of the menu item names
   string items_text[]=
     {
      "Item 1",
      "Item 2",
      "Item 3"
     };
//--- Array of icons for the available mode
   string items_bmp_on[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp"
     };
//--- Array of icon for the blocked mode 
   string items_bmp_off[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_colorless.bmp"
     };
//--- Set up properties before creation
   m_split_button1.ButtonXSize(116);
   m_split_button1.ButtonYSize(22);
   m_split_button1.DropButtonXSize(16);
   m_split_button1.LabelColor(clrBlack);
   m_split_button1.LabelColorPressed(clrBlack);
   m_split_button1.BackColor(clrGainsboro);
   m_split_button1.BackColorHover(C'193,218,255');
   m_split_button1.BackColorPressed(C'190,190,200');
   m_split_button1.BorderColor(C'150,170,180');
   m_split_button1.BorderColorOff(C'178,195,207');
   m_split_button1.BorderColorHover(C'150,170,180');
   m_split_button1.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp");
   m_split_button1.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp");
//--- Get the pointer to the context menu of the button
   CContextMenu *cm=m_split_button1.GetContextMenuPointer();
//--- Set up properties of the context menu
   cm.AreaBackColor(C'240,240,240');
   cm.AreaBorderColor(clrSilver);
   cm.ItemBackColor(C'240,240,240');
   cm.ItemBorderColor(C'240,240,240');
   cm.LabelColor(clrBlack);
   cm.LabelColorHover(clrWhite);
   cm.SeparateLineDarkColor(C'160,160,160');
   cm.SeparateLineLightColor(clrWhite);
//--- Add items to the context menu
   for(int i=0; i<CONTEXTMENU_ITEMS5; i++)
      m_split_button1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i]);
//--- Separation line after the first menu item
   m_split_button1.AddSeparateLine(1);
//--- Create control
   if(!m_split_button1.CreateSplitButton(m_chart_id,button_text,m_subwin,x,y))
      return(false);
//--- Add the control pointer to the base
   CWndContainer::AddToElementsArray(0,m_split_button1);
   return(true);
  }

 

You should see the following result after compiling files and launching the program on the chart:

Fig. 6. Test of the split button control.

Fig. 6. Test of the split button control.

 

The development of the class of the split button control is finished. You can download the version of the EA presented in the screenshot above in the files attached to this article. In the next article, we will consider the development of classes for creating button groups, that is interconnected buttons. 

 


Conclusion

This article was dedicated to creating simple and multi-functional buttons. In the next article, we will enrich our library with the classes for creating button groups.

The archives below with library files at the current stage of development, pictures and files of the programs considered in this article can be downloaded for tests in the MetaTrader terminals. If you have questions on using material from 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 third part:

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/2296

Attached files |
Step on New Rails: Custom Indicators in MQL5 Step on New Rails: Custom Indicators in MQL5

I will not list all of the new possibilities and features of the new terminal and language. They are numerous, and some novelties are worth the discussion in a separate article. Also there is no code here, written with object-oriented programming, it is a too serous topic to be simply mentioned in a context as additional advantages for developers. In this article we will consider the indicators, their structure, drawing, types and their programming details, as compared to MQL4. I hope that this article will be useful both for beginners and experienced developers, maybe some of them will find something new.

Here Comes the New MetaTrader 5 and MQL5 Here Comes the New MetaTrader 5 and MQL5

This is just a brief review of MetaTrader 5. I can't describe all the system's new features for such a short time period - the testing started on 2009.09.09. This is a symbolical date, and I am sure it will be a lucky number. A few days have passed since I got the beta version of the MetaTrader 5 terminal and MQL5. I haven't managed to try all its features, but I am already impressed.

False trigger protection for Trading Robot False trigger protection for Trading Robot

Profitability of trading systems is defined not only by logic and precision of analyzing the financial instrument dynamics, but also by the quality of the performance algorithm of this logic. False trigger is typical for low quality performance of the main logic of a trading robot. Ways of solving the specified problem are considered in this article.

Using text files for storing input parameters of Expert Advisors, indicators and scripts Using text files for storing input parameters of Expert Advisors, indicators and scripts

The article describes the application of text files for storing dynamic objects, arrays and other variables used as properties of Expert Advisors, indicators and scripts. The files serve as a convenient addition to the functionality of standard tools offered by MQL languages.