Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава 3)

Anatoli Kazharski | 29 февраля, 2016

Содержание

 

 

Введение

Более подробно о том, для чего предназначена эта библиотека, можно прочитать в первой статье серии: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце каждой части статей представлен полный список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.   

В предыдущих главах второй части серии статей о графических интерфейсах были реализованы классы для создания всех составных частей главного меню. Но перед тем, как начать разработку класса каждого элемента, нужно более точно настроить обработчики событий в главных базовых классах и в классах созданных элементов управления. В этой статье будут рассмотрены следующие вопросы:

Кроме этого, будет показано, как принимать сообщения в обработчике пользовательского класса приложения. 

 


Персональные массивы элементов

Проведем небольшой эксперимент. Нажмем левую кнопку мыши на одном из пунктов контекстного меню в той его области, где курсор мыши будет вне области формы. Мы увидим, что прокрутка графика не отключена, и её можно осуществлять, находясь над элементом управления. Это ошибка в функционале, такого быть не должно. Поэтому далее мы сделаем так, чтобы, над каким элементом управления ни находился бы курсор мыши, прокрутка графика и режим перемещения торговых уровней в этот момент были отключены. 

В первую очередь, добавим в обработчик контекстного меню отслеживание фокуса на элементе (см. код ниже). Если контекстное меню скрыто, то действовать дальше смысла нет. Такого подхода стоит придерживаться везде для экономии времени.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, элемент скрыт
      if(!CElement::m_is_visible)
         return;
      //--- Получим фокус
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
     }
  }

На текущем этапе разработки библиотеки в базе элементов, а именно — в классе CWndContainer, есть общий массив указателей элементов m_elements[]. Он является частью структуры массивов элементов WindowElements. Этот массив подходит для всех случаев, когда нужно применить какое-либо действие ко всем элементам управления или хотя бы к их большинству. Если же нужно применить действие лишь к определённой группе элементов, то такой подход чрезмерен, потому что тратит слишком много ресурсов. Например, есть группа элементов управления, при взаимодействии с которыми их размеры могут выйти за границы области формы, к которой они присоединены. К этой группе относятся различные выпадающие списки, в том числе и контекстные меню. Каждый тип таких элементов нужно сохранять в отдельных массивах. Управлять ими после этого станет намного удобнее и эффективнее. 

Добавим в структуру WindowElements массив для контекстных меню и создадим метод для получения его размера:

//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Общий массив всех объектов
      CChartObject     *m_objects[];
      //--- Общий массив всех элементов
      CElement         *m_elements[];
      
      //--- Персональные массивы элементов:
      //    Массив контекстных меню
      CContextMenu     *m_context_menus[];
     };
   //--- Массив массивов элементов для каждого окна
   WindowElements    m_wnd[];
   //---
public:
   //--- Количество контекстных меню
   int               ContextMenusTotal(const int window_index);
   //---
  };
//+------------------------------------------------------------------+
//| Возвращает кол-во контекстных меню по указанному индексу окна    |
//+------------------------------------------------------------------+
int CWndContainer::ContextMenusTotal(const int window_index)
  {
   if(window_index>=::ArraySize(m_wnd))
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return(::ArraySize(m_wnd[window_index].m_context_menus));
  }

Каждый раз после создания элемента управления в пользовательском классе приложения (в нашем случае CProgram), чтобы добавлять указатель на этот элемент в базу,  мы используем метод CWndContainer::AddToElementsArray(). В нем для каждого сложного (составного) элемента будут использоваться методы, предназначенные для получения и сохранения указателей на них в общий массив. Для контекстного меню ранее уже был создан такой метод — CWndContainer::AddContextMenuElements(). Во всех подобных методах можно осуществлять распределение указателей в персональные массивы элемента, если в этом есть такая необходимость.

Затем нам понадобится шаблонный метод для добавления указателя на элемент в массив, переданный по ссылке, потому что это действие будет производиться неоднократно и с разными типами объектов.

class CWndContainer
  {
protected:
   //--- Шаблонный метод для добавления указателей в переданный по ссылке массив
   template<typename T1,typename T2>
   void              AddToRefArray(T1 &object,T2 &ref_array[]);
   //---
  };
//+------------------------------------------------------------------+
//| Сохраняет указатель (T1) в переданный по ссылке массив (T2)      |
//+------------------------------------------------------------------+
template<typename T1,typename T2>
void CWndContainer::AddToRefArray(T1 &object,T2 &array[])
  {
   int size=::ArraySize(array);
   ::ArrayResize(array,size+1);
   array[size]=object;
  }

Теперь в конце метода CWndContainer::AddContextMenuElements() можно сохранить указатель на контекстное меню в его персональном массиве, как это показано ниже (отмечено жёлтым маркером). Таким же образом поступим для всех других элементов управления.

//+------------------------------------------------------------------+
//| Сохраняет указатели на объекты контекстного меню в базу          |
//+------------------------------------------------------------------+
bool CWndContainer::AddContextMenuElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не контекстное меню
   if(object.ClassName()!="CContextMenu")
      return(false);
//--- Получим указатель на контекстное меню
   CContextMenu *cm=::GetPointer(object);
//--- Сохраним указатели на его объекты в базе
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Увеличение массива элементов
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Получение указателя на пункт меню
      CMenuItem *mi=cm.ItemPointerByIndex(i);
      //--- Сохраняем указатель в массив
      m_wnd[window_index].m_elements[size]=mi;
      //--- Добавляем указатели на все объекты пункта меню в общий массив
      AddToObjectsArray(window_index,mi);
     }
//--- Добавим указатель в персональный массив
   AddToRefArray(cm,m_wnd[window_index].m_context_menus);
   return(true);
  } 

 


Управление состоянием графика

Далее в классе CWndEvents нужно добавить метод, в котором будут осуществляться проверки фокуса курсора мыши над тем или иным элементом управления. Такая проверка будет распространяться на формы и на выпадающие списки. И для форм, и для контекстных меню уже есть персональные массивы, поэтому создадим метод CWndEvents::SetChartState(). Ниже представлены объявление и имплементация этого метода:

class CWndEvents : public CWndContainer
  {
private:
   //--- Установка состояния графика
   void              SetChartState(void);
  };
//+------------------------------------------------------------------+
//| Устанавливает состояние графика                                  |
//+------------------------------------------------------------------+
void CWndEvents::SetChartState(void)
  {
//--- Для определения события, когда нужно отключить управление
   bool condition=false;
//--- Проверим окна
   int windows_total=CWndContainer::WindowsTotal();
   for(int i=0; i<windows_total; i++)
     {
      //--- Перейти к следующей, если эта форма скрыта
      if(!m_windows[i].IsVisible())
         continue;
      //--- Проверить условия во внутреннем обработчике формы
      m_windows[i].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
      //--- Если есть фокус, отметим это
      if(m_windows[i].MouseFocus())
        {
         condition=true;
         break;
        }
     }
//--- Проверим фокус контекстных меню
   if(!condition)
     {
      int context_menus_total=CWndContainer::ContextMenusTotal(0);
      for(int i=0; i<context_menus_total; i++)
        {
         if(m_wnd[0].m_context_menus[i].MouseFocus())
           {
            condition=true;
            break;
           }
        }
     }
//---
   if(condition)
     {
      //--- Отключим скролл и управление торговыми уровнями
      m_chart.MouseScroll(false);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,false);
     }
   else
     {
      //--- Включим управление
      m_chart.MouseScroll(true);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,true);
     }
  }

В этот метод в дальнейшем еще будут внесены некоторые дополнения, но выполнять текущую задачу он уже готов. Вызывать его нужно в методе CWndEvents::ChartEventMouseMove(), как это показано в коде ниже:

//+------------------------------------------------------------------+
//| Событие CHARTEVENT MOUSE MOVE                                    |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventMouseMove(void)
  {
//--- Выйти, если это не событие перемещения курсора
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      return;
//--- Перемещение окна
   MovingWindow();
//--- Установка состояния графика
   SetChartState();
//--- Перерисуем график
   m_chart.Redraw();
  }

Теперь скомпилируем все файлы и протестируем эксперта. Мы видим, что теперь при нажатии левой кнопкой мыши в области контекстного меню, которая выходит за границы области формы, режимы прокрутки графика и управления торговыми уровнями отключены. Тест установки элемента на график прошел успешно, и в дальнейшем контекстное меню будет вызываться только по запросу пользователя. Поэтому удалите его показ из метода CProgram::CreateTradePanel() в классе приложения (см. код ниже) .

   m_contextmenu.Show(); // <<< Эту строчку кода теперь нужно удалить

 


Идентификаторы для внешнего и внутреннего использования

Теперь займёмся обработкой нажатия левой кнопки мыши на пункте меню.

Следующая наша задача — сделать так, чтобы при клике на пункт меню мы получали бы контекстное меню, содержащееся в нем (если это вложение есть). При повторном нажатии оно должно скрываться. Такая обработка будет и в классе пункта меню CMenuItem, и в классе контекстного меню CContextMenu. Дело в том, что у контекстного меню есть доступ к тому пункту (предыдущему узлу), к которому оно привязано, а у пункта меню, которое содержит контекстное меню, нет прямого доступа к нему. В классе CMenuItem не получится создать указатель на контекстное меню, поскольку, если подключить сейчас файл ContextMenu.mqh к файлу MenuItem.mqh, то не получится произвести компиляцию без ошибок. Поэтому в классе CContextMenu мы будем производить обработку на показ контекстного меню. Обработчик в классе CMenuItem будет вспомогательным. Он будет генерировать пользовательское событие, отсылая контекстному меню уточняющие сведения о том, на каком именно пункте было произведено нажатие. Кроме этого, нужно ещё сделать так, чтобы при клике вне области контекстного меню оно скрывалось, подобно тому, как это сделано, например, в терминалах MetaTrader или редакторе кода MetaEditor. Да и в целом, это довольно стандартное поведение для контекстных меню. 

Для реализации этого функционала нам понадобятся дополнительные идентификаторы для пользовательских событий. Некоторые из них будут предназначены для внутреннего использования в классах библиотеки, а некоторые — для внешней обработки в классе пользовательского приложения (в нашем случае это CProgram). 

События для внутреннего использования:

Для внешнего использования создадим идентификатор, который будет сообщать программе о том, что произошло нажатие в пункте контекстного меню — ON_CLICK_CONTEXTMENU_ITEM.

В файле Defines.mqh поместим перечисленные идентификаторы с уникальными номерами для каждого:

#define ON_CLICK_MENU_ITEM        (4) // Нажатие на пункте меню
#define ON_CLICK_CONTEXTMENU_ITEM (5) // Нажатие на пункте меню в контекстном меню
#define ON_HIDE_CONTEXTMENUS      (6) // Скрыть все контекстные меню
#define ON_HIDE_BACK_CONTEXTMENUS (7) // Скрыть контекстные меню от текущего пункта меню

 


Доработка класса контекстного меню

В классе контекстного меню CContextMenu нужно добавить следующие поля и методы: 

В листинге кода ниже можно посмотреть объявления и имплементацию перечисленного в списке выше с подробными комментариями:

class CContextMenu : public CElement
  {
private:
   //--- Состояние контекстного меню
   bool              m_contextmenu_state;
public:   
   //--- (1) Получение и (2) установка состояния контекстного меню
   bool              ContextMenuState(void)                   const { return(m_context_menu_state);         }
   void              ContextMenuState(const bool flag)              { m_context_menu_state=flag;            }
   //---
private:
   //--- Обработка нажатия на пункте, к которому это контекстное меню привязано
   bool              OnClickMenuItem(const string clicked_object);
   //--- Получение (1) идентификатора и (2) индекса из имени пункта меню
   int               IdFromObjectName(const string object_name);
   int               IndexFromObjectName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на пункт меню                                  |
//+------------------------------------------------------------------+
bool CContextMenu::OnClickMenuItem(const string clicked_object)
  {
//--- Выйдем, если контекстное меню уже открыто
   if(m_contextmenu_state)
      return(true);
//--- Выйдем, если нажатие было не на пункте меню
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0)
      return(false);
//--- Получим идентификатор и индекс из имени объекта
   int id    =IdFromObjectName(clicked_object);
   int index =IndexFromObjectName(clicked_object);
//--- Выйдем, если нажали не на пункте, к которому это контекстное меню привязано
   if(id!=m_prev_node.Id() || index!=m_prev_node.Index())
      return(false);
//--- Показать контекстное меню
   Show();
   return(true);
  }
//+------------------------------------------------------------------+
//| Извлекает идентификатор из имени объекта                         |
//+------------------------------------------------------------------+
int CContextMenu::IdFromObjectName(const string object_name)
  {
//--- Получим id из имени объекта
   int    length =::StringLen(object_name);
   int    pos    =::StringFind(object_name,"__",0);
   string id     =::StringSubstr(object_name,pos+2,length-1);
//---
   return((int)id);
  }
//+------------------------------------------------------------------+
//| Извлекает индекс из имени объекта                                |
//+------------------------------------------------------------------+
int CContextMenu::IndexFromObjectName(const string object_name)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Получим код разделителя
   u_sep=::StringGetCharacter("_",0);
//--- Разобьём строку
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-1;
//--- Проверка выхода за диапазон массива
   if(array_size-2<0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return((int)result[array_size-2]);
  }

В обработчик событий контекстного меню CContextMenu::OnEvent() теперь достаточно добавить вызов метода CContextMenu::OnClickMenuItem() по приходу события CHARTEVENT_OBJECT_CLICK:

//--- Обработка события нажатия левой кнопки мыши на объекте
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickMenuItem(sparam))
         return;
     }

 


Доработка класса пункта меню

В итоге, обнаружив нажатие левой кнопкой мыши на пункт меню, программа передаст строковой параметр в метод CContextMenu::OnClickMenuItem(). В строковом параметре содержится имя нажатого графического объекта «прямоугольная метка», которая является фоном пункта меню. Помним, что приоритет по клику практически у всех элементов управления будет выше для фона, чем чем для остальных объектов элемента. Это гарантирует, что нажатие не будет "перехвачено" другим объектом элемента, что, в свою очередь, может спровоцировать неожиданное поведение программы. Например, если приоритет будет выше у ярлыка пункта меню, чем у его фона, то нажатие в области ярлыка может привести к изменению его картинки (помним, что картинки ярлыка у нас определены для двух состояний). Причина этого — в том, что такое поведение по умолчанию заложено у всех объектов типа OBJ_BITMAP_LABEL

В самом начале метода CContextMenu::OnClickMenuItem() будет производиться проверка состояния контекстного меню. Если оно уже включено, то действовать дальше нет смысла. Затем проверяется имя нажатого объекта. Если это объект нашей программы и есть признак того, что это пункт меню, то идём дальше. Затем из имени объекта извлекается идентификатор и индекс пункта меню. Для этих задач мы уже подготовили специальные методы, в которых с помощью строковых функций языка MQL извлекаются необходимые параметры из имени объекта. Идентификатор пункта меню извлекается по признаку двойного прочерка. Для извлечения индекса строка разбивается на части по символу «нижний прочерк» (_), который является разделителем параметров объекта элемента.

Затем в классе CMenuItem также создадим метод OnClickMenuItem(), но его код будет отличаться от того, который был разработан для контекстного меню. Ниже показаны объявление и имплементация этого метода. В нем уже нет необходимости извлекать параметры из имени объекта. Достаточно просто сравнить имя фона с названием переданного объекта. Далее проверяется текущее состояние пункта: если он заблокирован, то дальнейших действий не производится. После этого, если пункт содержит контекстное меню, устанавливается статус включенного или отключенного элемента. Если до этого момента состояние контекстного меню было «включенное», то в главный модуль обработки событий отправляется сигнал на закрытие всех контекстных меню, открытых позже. Это работает для тех случаев, когда одновременно открыты несколько контекстных меню, открывающихся одно из другого. Такие примеры будут показаны далее в статье. Кроме идентификатора события ON_HIDE_BACK_CONTEXTMENUS, в качестве ещё одного параметра отправляется идентификатор пункта меню, с помощью которого можно определить, на каком контекстном меню нужно будет остановить цикл.

class CMenuItem : public CElement
  {
   //--- Обработка нажатия на пункте меню
   bool              OnClickMenuItem(const string clicked_object);
   //---
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на пункте меню                                 |
//+------------------------------------------------------------------+
bool CMenuItem::OnClickMenuItem(const string clicked_object)
  {
//--- Проверка по имени объекта
   if(m_area.Name()!=clicked_object)
      return(false);
//--- Выйдем, если пункт неактивирован
   if(!m_item_state)
      return(false);
//--- Если этот пункт содержит контекстное меню
   if(m_type_menu_item==MI_HAS_CONTEXT_MENU)
     {
      //--- Если выпадающее меню этого пункта не активировано
      if(!m_context_menu_state)
        {
         m_context_menu_state=true;
        }
      else
        {
         m_context_menu_state=false;
         //--- Отправим сигнал для закрытия контекстных меню дальше этого пункта
         ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,"");
        }
      return(true);
     }
//--- Если этот пункт не содержит контекстное меню, но является частью контекстного меню
   else
     {
     }
//---
   return(true);
  }

 

 


Доработка главного класса обработки событий графического интерфейса

Это не окончательная версия метода CMenuItem::OnClickMenuItem(), и мы ещё вернёмся к нему позже, чтобы внести дополнения. На текущий момент его основная задача заключается лишь в том, чтобы отсылать сообщение на скрытие контекстного меню в главный модуль обработки пользовательских событий в классе CWndEvents. Перейдем в него и создадим метод, доступ в который будет осуществляться по событию ON_HIDE_BACK_CONTEXTMENUS. Назовём его CWndEvents::OnHideBackContextMenus(). Этот метод представлен для подробного изучения в коде:

class CWndEvents : public CWndContainer
  {
private:
   //--- Скрытие всех контекстных меню от пункта инициатора
   bool              OnHideBackContextMenus(void);
  };
//+------------------------------------------------------------------+
//| Событие ON_HIDE_BACK_CONTEXTMENUS                                |
//+------------------------------------------------------------------+
bool CWndEvents::OnHideBackContextMenus(void)
  {
//--- Если сигнал на скрытие контекстных меню от пункта инициатора
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_BACK_CONTEXTMENUS)
      return(false);
//--- Пройдём по всем меню от последнего вызванного
   int context_menus_total=CWndContainer::ContextMenusTotal(0);
   for(int i=context_menus_total-1; i>=0; i--)
     {
      //--- Указатели контекстного меню и его предыдущего узла
      CContextMenu *cm=m_wnd[0].m_context_menus[i];
      CMenuItem    *mi=cm.PrevNodePointer();
      //--- Если дошли до пункта инициатора сигнала, то...
      if(mi.Id()==m_lparam)
        {
         //--- ...в случае, если его контекстное меню без фокуса, скроем его
         if(!cm.MouseFocus())
            cm.Hide();
         //--- Завершим цикл
         break;
        }
      else
        {
         //--- Скрыть контекстное меню
         cm.Hide();
        }
     }
//---
   return(true);
  }

Метод CWndEvents::OnHideBackContextMenus() нужно вызывать в методе обработки пользовательских событий, как это показано ниже:

//+------------------------------------------------------------------+
//| Событие CHARTEVENT_CUSTOM                                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- Если поступает сигнал свернуть форму
   if(OnWindowRollUp())
      return;
//--- Если поступает сигнал развернуть форму
   if(OnWindowUnroll())
      return;
//--- Если поступает сигнал на скрытие контекстных меню от пункта инициатора
   if(OnHideBackContextMenus())
      return;
  }

 


Предварительный тест обработчиков событий

После того, как внесены все изменения, нужно скомпилировать все файлы и загрузить программу на график для тестирования. Теперь при нажатии на независимый пункт меню на форме его контекстное меню будет появляться, если было скрыто до этого, и, наоборот, скрываться, если было открыто. При этом, когда контекстное меню открыто, то цвет фона пункта меню на форме будет фиксироваться — то есть, не будет вновь изменяться, если убрать курсор мыши из его области (см. скриншот ниже). 

Рис. 1. Тест показа и скрытия контекстного меню.

Рис. 1. Тест показа и скрытия контекстного меню.

 

Продолжим настраивать взаимодействие контекстного меню с пользователем. В большинстве приложений, когда открыто одно либо несколько контекстных меню (одно из другого), при клике мышью в области вне их границ все они сразу закроются. Точно такое же поведение реализуем и здесь. 

Чтобы впоследствии можно было полноценно протестировать этот функционал, расширим ещё одним контекстным меню интерфейс нашего эксперта. Добавим в третий пункт уже имеющегося контекстного меню ещё одно контекстное меню. Для этого достаточно в методе создания первого контекстного меню CProgram::CreateContextMenu1() в массиве items_type[] присвоить третьему элементу тип MI_HAS_CONTEXT_MENU:

//--- Массив типов пунктов
   ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS]=
     {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_HAS_CONTEXT_MENU,
      MI_CHECKBOX,
      MI_CHECKBOX
     };

Теперь создадим метод для второго контекстного меню. Добавьте в класс CProgram второй экземпляр класса CContextMenu и объявите метод CreateContextMenu2():

class CProgram : public CWndEvents
  {
private:
   //--- Пункт меню и контекстные меню
   CMenuItem         m_menu_item1;
   CContextMenu      m_mi1_contextmenu1;
   CContextMenu      m_mi1_contextmenu2;
   //---
private:
#define MENU_ITEM1_GAP_X (6)
#define MENU_ITEM1_GAP_Y (25)
   bool              CreateMenuItem1(const string item_text);
   bool              CreateMI1ContextMenu1(void);
   bool              CreateMI1ContextMenu2(void);
  };

Второе контекстное меню будет состоять из шести пунктов. Пусть это будут две группы радио-пунктов (MI_RADIOBUTTON), по три в каждой. Ниже показан код этого метода. Чем он отличается от метода создания первого контекстного меню? Обратите внимание, как происходит получение указателя на третий пункт первого контекстного меню, к которому нужно привязать второе. Для этого ранее уже был создан специальный метод CContextMenu::ItemPointerByIndex(). Картинки для радио-пунктов будем использовать по умолчанию, поэтому массивы для них не нужны, а в метод CContextMenu::AddItem() вместо пути к картинкам в таком случае необходимо передать пустые значения. Разделительная линия нужна, чтобы визуально отделить первую группу радио-пунктов от второй, поэтому размещаем её после третьего (2) пункта в списке.

Ранее уже было рассказано и показано на схеме, что у каждой группы радио-пунктов должен быть свой уникальный идентификатор. По умолчанию значение этого параметра равно 0, поэтому для второй группы каждому радио-пункту присвоим (в цикле с третьего по шестой) идентификатор, равный 1. Для установки идентификатора в классе CContextMenu уже есть метод CContextMenu::RadioItemIdByIndex().

С помощью метода CContextMenu::SelectedRadioItem() установим, какие радио-пункты в каждой группе должны быть выделены изначально. В коде, приведенном ниже ниже, в первой группе выделяется второй (индекс 1) радио-пункт, а во второй группе — третий (индекс 2).

//+------------------------------------------------------------------+
//| Создаёт контекстное меню 2                                       |
//+------------------------------------------------------------------+
bool CProgram::CreateMI1ContextMenu2(void)
  {
//--- Шесть пунктов в контекстном меню
#define CONTEXTMENU_ITEMS2 6
//--- Сохраним указатель на окно
   m_mi1_contextmenu2.WindowPointer(m_window);
//--- Сохраним указатель на предыдущий узел
   m_mi1_contextmenu2.PrevNodePointer(m_mi1_contextmenu1.ItemPointerByIndex(2));
//--- Массив названий пунктов
   string items_text[CONTEXTMENU_ITEMS2]=
     {
      "ContextMenu 2 Item 1",
      "ContextMenu 2 Item 2",
      "ContextMenu 2 Item 3",
      "ContextMenu 2 Item 4",
      "ContextMenu 2 Item 5",
      "ContextMenu 2 Item 6"
     };
//--- Установим свойства перед созданием
   m_mi1_contextmenu2.XSize(160);
   m_mi1_contextmenu2.ItemYSize(24);
   m_mi1_contextmenu2.AreaBackColor(C'240,240,240');
   m_mi1_contextmenu2.AreaBorderColor(clrSilver);
   m_mi1_contextmenu2.ItemBackColorHover(C'240,240,240');
   m_mi1_contextmenu2.ItemBackColorHoverOff(clrLightGray);
   m_mi1_contextmenu2.ItemBorderColor(C'240,240,240');
   m_mi1_contextmenu2.LabelColor(clrBlack);
   m_mi1_contextmenu2.LabelColorHover(clrWhite);
   m_mi1_contextmenu2.SeparateLineDarkColor(C'160,160,160');
   m_mi1_contextmenu2.SeparateLineLightColor(clrWhite);
//--- Добавить пункты в контекстное меню
   for(int i=0; i<CONTEXTMENU_ITEMS2; i++)
      m_mi1_contextmenu2.AddItem(items_text[i],"","",MI_RADIOBUTTON);
//--- Разделительная линия после третьего пункта
   m_mi1_contextmenu2.AddSeparateLine(2);
//--- Для второй группы установим уникальный идентификатор (1)
   for(int i=3; i<6; i++)
      m_mi1_contextmenu2.RadioItemIdByIndex(i,1);
//--- Выбор радио-пунктов в обоих группах
   m_mi1_contextmenu2.SelectedRadioItem(1,0);
   m_mi1_contextmenu2.SelectedRadioItem(2,1);
//--- Создать контекстное меню
   if(!m_mi1_contextmenu2.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_mi1_contextmenu2);
   return(true);
  }

 

Вызов метода CProgram::CreateContextMenu2() размещается в методе CProgram::CreateTradePanel(), как и для всех остальных.

 

 

Тест нескольких контекстных меню и тонкая настройка

Скомпилировав файлы эксперта и загрузив его на график, можно увидеть такой результат:

Рис. 2. Тест нескольких контекстных меню.

Рис. 2. Тест нескольких контекстных меню.

 

Когда оба контекстных меню открыты (как показано на скриншоте выше) при клике на пункте, который вызывает первое меню, закроются оба. Именно такое поведение было заложено в методе CWndEvents::OnHideBackContextMenus(), который мы рассмотрели немного выше. Но если кликнуть сейчас на графике или заголовке формы, то контекстные меню не закроются. Далее займёмся решением этой задачи.

Местоположение курсора мыши (фокус) определяется в обработчике событий OnEvent() класса контекстного меню (CContextMenu). Поэтому отправлять сигнал на закрытие всех открытых контекстных меню в главный обработчик событий (в классе CWndEvents) будем оттуда же. Задача решается следующим образом.

1. В момент прихода события перемещения мыши (CHARTEVENT_MOUSE_MOVE) строковой параметр (sparam) содержит состояние левой кнопки мыши.

2. Далее, после определения фокуса мыши, строим ветку с проверкой текущего состояния контекстного меню и левой кнопки мыши. Если контекстное меню активировано и кнопка нажата, то переходим к следующей проверке, где определяется текущее местоположение курсора относительно этого контекстного меню и предыдущего узла.

3. Если курсор находится в области одного из них, то не нужно отправлять сигнал на закрытие всех контекстных меню. Если же курсор вне области этих элементов, то нужно проверить, есть ли контекстные меню, которые были открыты позже.

4. Для этого нужно пройтись в цикле по списку этого контекстного меню для определения, есть ли в нём пункт, содержащий своё контекстное меню и активировано ли оно, если такой пункт нашёлся. Если оказалось, что оно активировано, значит может оказаться, что курсор находится в его области, а это значит, что и в этом случае не нужно отсылать сигнал на закрытие всех контекстных меню из этого элемента. Но если оказалось, что текущее контекстное меню было открыто последним и во всех предыдущих до него тоже не исполнились все условия для отправки сигнала, то это уже железно означает, что курсор находится вне областей всех активированных контекстных меню.

5. Именно здесь можно генерировать пользовательское событие ON_HIDE_CONTEXTMENUS.

Как видим, ключевой момент заключается в том, что закрывать все контекстные меню нужно только тогда, когда курсор мыши (если при этом левая кнопка мыши зажата) находится вне области последнего активированного контекстного меню и вне области пункта, из которого оно было вызвано.

В следующем листинге кода можно посмотреть, как выглядит описанная выше логика. Для этого был написан отдельный метод CContextMenu::CheckHideContextMenus().

class CContextMenu : public CElement
  {
private:
   //--- Проверка условий на закрытие всех контекстных меню
   void              CheckHideContextMenus(void);
   //---
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::m_is_visible)
         return;
      //--- Получим фокус
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Если контекстное меню включено и левая кнопка мыши нажата
      if(m_context_menu_state && sparam=="1")
        {
         //--- Проверка условий на закрытие всех контекстных меню
         CheckHideContextMenus();
         return;
        }
      //---
      return;
     }
  }
//+------------------------------------------------------------------+
//| Проверка условий на закрытие всех контекстных меню               |
//+------------------------------------------------------------------+
void CContextMenu::CheckHideContextMenus(void)
  {
//--- Выйти, если курсор находится в области контекстного меню или в области предыдущего узла
   if(CElement::MouseFocus() || m_prev_node.MouseFocus())
      return;
//--- Если же курсор вне области этих элементов, то ...
//    ... нужно проверить, есть ли открытые контекстные меню, которые были активированы после этого
//--- Для этого пройдёмся в цикле по списку этого контекстного меню ...
//    ... для определения наличия пункта, который содержит в себе контекстное меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Если такой пункт нашёлся, то нужно проверить, открыто ли его контекстное меню.
      //    Если оно открыто, то не нужно отсылать сигнал на закрытие всех контекстных меню из этого элемента, так как...
      //    ... возможно, что курсор находится в области следующего, и нужно проверить там.
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU)
         if(m_items[i].ContextMenuState())
            return;
     }
//--- Послать сигнал на скрытие всех контекстных меню
   ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
  }

Теперь событие ON_HIDE_CONTEXTMENUS нужно принять в главном обработчике разрабатываемой библиотеки в классе CWndEvents. Для этого напишем специальный метод, который назовём OnHideContextMenus(). Он довольно прост, так как в его задачи сейчас входит только пройтись в цикле по персональному массиву контекстных меню и скрыть их. 

В листинге кода ниже — объявление и имплементация метода CWndEvents::OnHideContextMenus():

class CWndEvents : public CWndContainer
  {
private:
   //--- Скрытие всех контекстных меню
   bool              OnHideContextMenus(void);
  };
//+------------------------------------------------------------------+
//| Событие ON_HIDE_CONTEXTMENUS                                     |
//+------------------------------------------------------------------+
bool CWndEvents::OnHideContextMenus(void)
  {
//--- Если сигнал на скрытие всех контекстных меню
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS)
      return(false);
//---
   int cm_total=CWndContainer::ContextMenusTotal(0);
   for(int i=0; i<cm_total; i++)
      m_wnd[0].m_context_menus[i].Hide();
//---
   return(true);
  }

После компиляции файлов библиотеки и загрузки эксперта для тестов на график уже можно убедиться, что если кликнуть мышью вне областей активированных контекстных меню, они будут скрываться.

Перейдем еще к одной заметной недоработке, которую нужно устранить. Обратите внимание на скриншот ниже. На нём показана ситуация, когда курсор мыши находится в области первого контекстного меню, но вне области пункта вызова второго контекстного меню. Обычно при возникновении таких ситуаций закрываются все контекстные меню "младше" того, в котором сейчас находится курсор. Напишем код, который обеспечит такое поведение.

Рис. 3. В такой ситуации все контекстные меню справа должны быть скрыты.

Рис. 3. В такой ситуации все контекстные меню справа должны быть скрыты.

 

Следующий метод назовём CContextMenu::CheckHideBackContextMenus(). Его логика уже была описана в предыдущем абзаце, поэтому можно сразу представить его реализацию (см. листинг кода ниже). Если все условия выполняются, то генерируется событие ON_HIDE_BACK_CONTEXTMENUS

class CContextMenu : public CElement
  {
private:
   //--- Проверка условий на закрытие всех контекстных меню, которые были открыты после этого
   void              CheckHideBackContextMenus(void);
   //---
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::m_is_visible)
         return;
      //--- Получим фокус
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Если контекстное меню включено и левая кнопка мыши нажата
      if(m_context_menu_state && sparam=="1")
        {
         //--- Проверка условий на закрытие всех контекстных меню
         CheckHideContextMenus();
         return;
        }
      //--- Проверка условий на закрытие всех контекстных меню, которые были открыты после этого
      CheckHideBackContextMenus();
      return;
     }
  }
//+------------------------------------------------------------------+
//| Проверка условий на закрытие всех контекстных меню,              |
//| которые были открыты после этого                                 |
//+------------------------------------------------------------------+
void CContextMenu::CheckHideBackContextMenus(void)
  {
//--- Пройтись по всем пунктам меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Если пункт содержит контекстное меню и оно включено
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU && m_items[i].ContextMenuState())
        {
         //--- Если фокус в контекстном меню, но не в этом пункте
         if(CElement::MouseFocus() && !m_items[i].MouseFocus())
            //--- Отправить сигнал на скрытие всех контекстных меню, которые были открыты после этого
            ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,"");
        }
     }
  }

Ранее в классе CWndEvents уже был написан метод OnHideBackContextMenus() для обработки события ON_HIDE_BACK_CONTEXTMENUS, поэтому сейчас уже можно скомпилировать файлы проекта и протестировать эксперта. Если всё сделано правильно, то теперь контекстные меню будут реагировать на перемещение курсора мыши в соответствии с условиями поставленной задачи.

Самое сложное позади, но работа еще не закончена. Далее нужно настроить обработчики событий так, чтобы при клике на пунктах контекстного меню в пользовательский класс приложения (CProgram) отправлялось событие со значениями параметров, позволяющими определить, на какой именно пункт меню был совершен клик. Так разработчик приложения сможет присваивать пунктам меню определенные функции. Кроме этого, нужно ещё настроить переключение состояний чекбоксов и радио-пунктов контекстного меню.

В классе CMenuItem в методе OnClickMenuItem() остался незаполненным блок для условия, когда пункт не содержит контекстное меню, но является его частью. Отсюда будет отправляться пользовательское событие ON_CLICK_MENU_ITEM, а в качестве дополнительных параметров сообщение будет содержать:

  1. Индекс общего списка. 
  2. Идентификатор элемента.
  3. Строку, которая будет формироваться из:

  • имени программы;
  • признака чекбокса или радио-пункта;
  • в случае, если это радио-пункт, то ещё и его идентификатора.

Как видите, когда не хватает возможностей функции EventChartCustom(), то всегда можно сформировать строку с нужным количеством параметров для точной идентификации. Подобно тому, как мы это делали с именами графических объектов, параметры будем разделять нижним прочерком «_».

И наконец, в этом же блоке будет изменяться состояние чекбокса или радио-пункта. Ниже показана сокращённая версия метода CMenuItem::OnClickMenuItem() только с тем кодом, который нужно добавить в блок else.

//+------------------------------------------------------------------+
//| Нажатие на заголовок элемента                                    |
//+------------------------------------------------------------------+
bool CMenuItem::OnClickMenuItem(const string clicked_object)
  {
//--- Проверка по имени объекта
//--- Выйдем, если пункт неактивирован
//--- Если этот пункт содержит контекстное меню
      //... 
//--- Если этот пункт не содержит контекстное меню, но является частью контекстного меню
   else
     {
      //--- Префикс сообщения с именем программы
      string message=CElement::ProgramName();
      //--- Если это чекбокс, изменим его состояние
      if(m_type_menu_item==MI_CHECKBOX)
        {
         m_checkbox_state=(m_checkbox_state)? false : true;
         m_icon.Timeframes((m_checkbox_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
         //--- Добавим в сообщение, что это чекбокс
         message+="_checkbox";
        }
      //--- Если это радио-пункт, изменим его состояние
      else if(m_type_menu_item==MI_RADIOBUTTON)
        {
         m_radiobutton_state=(m_radiobutton_state)? false : true;
         m_icon.Timeframes((m_radiobutton_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
         //--- Добавим в сообщение, что это радио-пункт
         message+="_radioitem_"+(string)m_radiobutton_id;
        }
      //--- Отправим сообщение об этом
      ::EventChartCustom(m_chart_id,ON_CLICK_MENU_ITEM,m_index,CElement::Id(),message);
     }
//---
   return(true);
  }

Пользовательское событие с идентификатором ON_CLICK_MENU_ITEM предназначено для обработчика класса контекстного меню (CContextMenu). Нам понадобятся дополнительные методы для извлечения идентификатора из строкового параметра события, если это было нажатие на радио-пункте, а также для получения индекса относительно той группы, которой радио-пункт принадлежит. Ниже можно посмотреть код этих методов.

Так как извлечение идентификатора из строкового параметра сообщения зависит от структуры передаваемой строки, то в методе CContextMenu::RadioIdFromMessage() будут дополнительные проверки на корректность сформированной строки и выход за пределы массива.

В методе CContextMenu::RadioIndexByItemIndex(), который предназначен для возвращения индекса радио-пункта по общему индексу, в самом начале получаем идентификатор радио-пункта по общему индексу, используя для этого ранее написанный метод CContextMenu::RadioItemIdByIndex(). После этого в цикле нужно просто посчитать радио-пункты с этим идентификатором, и дойдя до радио-пункта с общим индексом, значение которого равно переданному индексу, запомнить значение счётчика и остановить цикл. То есть, последнее значение счётчика и будет тем индексом, который нужно возвратить.

class CContextMenu : public CElement
  {
private:
   //--- Получение (1) идентификатора и (2) индекса из сообщения радио-пункта
   int               RadioIdFromMessage(const string message);
   int               RadioIndexByItemIndex(const int index);
   //---
  };
//+------------------------------------------------------------------+
//| Извлекает идентификатор из сообщения для радио-пункта            |
//+------------------------------------------------------------------+
int CContextMenu::RadioIdFromMessage(const string message)
  {
   ushort u_sep=0;
   string result[];
   int    array_size=0;
//--- Получим код разделителя
   u_sep=::StringGetCharacter("_",0);
//--- Разобьём строку
   ::StringSplit(message,u_sep,result);
   array_size=::ArraySize(result);
//--- Если структура сообщения отличается от ожидаемой
   if(array_size!=3)
     {
      ::Print(__FUNCTION__," > Неправильная структура в сообщении для радио-пункта! message: ",message);
      return(WRONG_VALUE);
     }
//--- Предотвращение выхода за пределы массива
   if(array_size<3)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//--- Вернуть id радио-пункта
   return((int)result[2]);
  }
//+------------------------------------------------------------------+
//| Возвращает индекс радио-пункта по общему индексу                 |
//+------------------------------------------------------------------+
int CContextMenu::RadioIndexByItemIndex(const int index)
  {
   int radio_index =0;
//--- Получаем ID радио-пункта по общему индексу
   int radio_id =RadioItemIdByIndex(index);
//--- Счётчик пунктов из нужной группы
   int count_radio_id=0;
//--- Пройдёмся в цикле по списку
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Если это не радио-пункт, перейти к следующему
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- Если идентификаторы совпадают
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Если индексы совпали, то 
         //    запомним текущее значение счётчика и закончим цикл
         if(m_items[i].Index()==index)
           {
            radio_index=count_radio_id;
            break;
           }
         //--- Увеличение счётчика
         count_radio_id++;
        }
     }
//--- Вернуть индекс
   return(radio_index);
  }

Далее создадим метод CContextMenu::ReceiveMessageFromMenuItem() для обработки пользовательского события ON_CLICK_MENU_ITEM от пункта меню. В этот метод нужно передать параметры события: идентификатор, индекс и строковое сообщение. В начале метода проверяются условия, от нашей ли программы поступило это сообщение и совпадают ли идентификаторы. Если да, то в случае, если это сообщение от радио-пункта, тут же производится переключение в группе, которая определяется идентификатором, а нужный пункт — индексом. Получить идентификатор и индекс можно с помощью созданных в предыдущем листинге методов. 

Независимо от типа пункта, от которого пришло сообщение, в случае успеха после проверки имени программы и сравнения идентификаторов отправляется пользовательское сообщение ON_CLICK_CONTEXTMENU_ITEM. Оно предназначено для обработчика в классе пользовательского приложения CProgram. Вместе с ним отправляются такие параметры, как: (1) идентификатор, (2) общий индекс в списке контекстного меню и (3) отображаемый текст пункта.

В самом конце метода, независимо от первой проверки, (1) контекстное меню скрывается, (2) форма разблокируется и (3) отсылается сигнал на закрытие всех контекстных меню.

class CContextMenu : public CElement
  {
private:
   //--- Приём сообщения от пункта меню для обработки
   void              ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item);
   //---
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события ON_CLICK_MENU_ITEM
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
     {
      int    item_id      =int(dparam);
      int    item_index   =int(lparam);
      string item_message =sparam;
      //--- Приём сообщения от пункта меню для обработки
      ReceiveMessageFromMenuItem(item_id,item_index,item_message);
      return;
     }
  }
//+------------------------------------------------------------------+
//| Приём сообщения от пункта меню для обработки                     |
//+------------------------------------------------------------------+
void CContextMenu::ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item)
  {
//--- Если есть признак того, что сообщение поступило от этой программы и id элемента совпадает
   if(::StringFind(message_item,CElement::ProgramName(),0)>-1 && id_item==CElement::Id())
     {
      //--- Если нажатие было на радио-пункте
      if(::StringFind(message_item,"radioitem",0)>-1)
        {
         //--- Получим id радио-пункта из переданного сообщения
         int radio_id=RadioIdFromMessage(message_item);
         //--- Получим индекс радио-пункта по общему индексу
         int radio_index=RadioIndexByItemIndex(index_item);
         //--- Переключить радио-пункт
         SelectedRadioItem(radio_index,radio_id);
        }
      //--- Отправим сообщение об этом
      ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,index_item,id_item,DescriptionByIndex(index_item));
     }
//--- Скрытие контекстного меню
   Hide();
//--- Разблокируем форму
   m_wnd.IsLocked(false);
//--- Послать сигнал на скрытие всех контекстных меню
   ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
  }

 

 


Тест прихода сообщений в пользовательский класс приложения

Сейчас мы можем в качестве теста попробовать принять такое сообщение в обработчике класса CProgram. Для этого добавьте в него код, как показано ниже:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_CONTEXTMENU_ITEM)
     {
      ::Print(__FUNCTION__," > index: ",lparam,"; id: ",int(dparam),"; description: ",sparam);
     }
  }

Теперь скомпилируйте файлы и загрузите эксперта на график. При клике на пунктах меню в журнал экспертов будут выводиться сообщения с параметрами этих пунктов:

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

Разработка основной части класса CContextMenu для создания контекстного меню завершена. В него нужно будет ещё вносить некоторые дополнения, но к этому вопросу мы подойдём чуть позже, а именно — когда та или иная проблема проявит себя при тестировании. Проще говоря, будем соблюдать естественную последовательность изложения. Так материал усваивается намного легче.

 


Заключение

В этой главе были доработаны классы элементов, созданные нами ранее (см. предыдущие статьи). Теперь у нас всё готово для разработки элемента «Главное меню». Решением этой задачи мы и займёмся в следующей статье.

Ниже вы можете загрузить на свой компьютер архивы с файлами библиотеки на текущей стадии разработки, изображения и файлы рассматриваемых в статье программ (эксперта, индикаторов и скрипта) для тестов в терминалах Metatrader 4 и Metatrader 5. Если у вас возникают вопросы по использованию материала, предоставленного в этих файлах, то вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей в представленном списке ниже или задать вопрос в комментариях к статье. 

Список статей (глав) второй части: