DoEasy. Элементы управления (Часть 11): WinForms-объекты — группы, WinForms-объект CheckedListBox

15 июля 2022, 14:48
Artyom Trishkin
0
387

Содержание


Концепция

WinForms-объекты, прикреплённые к одному контейнеру, становятся набором объектов, объединённых в одну группу. Не важно, прикреплены ли они к объекту GroupBox, или просто к какой-либо панели, этот контейнер становится для объектов сущностью, объединяющей их все в одну группу. И объекты начинают себя вести по определённым правилам этой группы. Например, объект RadioButton — он в единичном исполнении практически не несёт никакой пользы. Если он создан невыбранным, то после щелчка по нему его флажок выбора устанавливается, и больше его снять не получится. Чтобы снять этот флажок, необходимо выбрать другой такой же объект. И вот тут есть разница — если выбрать другой объект RadioButton в составе того же контейнера, то с первого флажок будет снят, а на втором, по которому был щелчок мышкой — установлен. Но если попробовать выбрать RadioButton, привязанный к другому контейнеру, то флажок с выделенного первого объекта, находящегося в первой группе, снят не будет. И это естественно — на то они и разные группы объектов в разных контейнерах.

Но что делать, если мы в одном контейнере хотим иметь два набора объектов RadioButton, работающих независимо друг от друга? Ведь они же объединены в один набор объектов своим контейнером (от которого должны наследовать номер его группы), и работают согласно общему номеру группы, полученному от своего контейнера. И вот для того, чтобы в одном контейнере сделать несколько независимо работающих наборов таких объектов, мы введём понятие группы объектов.

Тогда, если в контейнере с номером группы 1 создать набор из шести объектов RadioButton, то всем им будет присвоен номер группы 1, и они все будут связаны воедино номером группы своего контейнера и работать соответственно — нажатие на любом из шести объектов RadioButton снимет выделение для остальных пяти.

Но если четырём объектом группы 1 присвоить группу 2, а оставшимся двум — группу 3, то внутри контейнера с группой 1 будут созданы две подгруппы объектов — с номером группы 2 и номером группы 3. И, соответственно, каждая из этих новых групп будет работать только в связке с объектами своей группы.

Это позволит нам в одном контейнере создавать различные подгруппы, объединённые в свою группу под своим номером, и не нужно будет создавать для групп новые контейнеры внутри основного.

Таким образом, объединив две кнопки-переключателя в одну группу, мы сделаем эти две кнопки как двухкнопочный переключатель — когда нажатие на первую кнопку отожмёт вторую, и наоборот. Таким образом можно будет сделать так, что несколько Toggle-кнопок, объединённых в одну группу могут быть в состоянии либо все кнопки не нажаты, либо только одна из них зафиксирована в нажатом состоянии, а остальные при этом — в отжатом.

Ну и немного доработаем класс объекта-аккаунта, так как на некоторых серверах наименование терминала возвращается не стандартное. Обычно при запросе имени терминала сервер возвращает строку "MetaTrader 5", но оказывается бывает, что туда брокер что-либо своё дописывает, и наименование терминала становится иным. Поэтому для определения типа сервера мы будем проверять не имя терминала, а подстроку "MetaTrader 5" в его имени.


Доработка классов библиотеки

В файле \MQL5\Include\DoEasy\Defines.mqh в перечислении типов графических элементов впишем новый тип WinForms-объекта:

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Стандартный графический объект
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Расширенный стандартный графический объект
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Объект тени
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Элемент
   GRAPH_ELEMENT_TYPE_FORM,                           // Форма
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Окно
   //--- WinForms
   GRAPH_ELEMENT_TYPE_WF_UNDERLAY,                    // Подложка объекта-панели
   GRAPH_ELEMENT_TYPE_WF_BASE,                        // Windows Forms Base
   //--- Ниже нужно вписывать типы объектов "контейнер"
   GRAPH_ELEMENT_TYPE_WF_CONTAINER,                   // Windows Forms базовый объект-контейнер
   GRAPH_ELEMENT_TYPE_WF_PANEL,                       // Windows Forms Panel
   GRAPH_ELEMENT_TYPE_WF_GROUPBOX,                    // Windows Forms GroupBox
   //--- Ниже нужно вписывать типы объектов "стандартный элемент управления"
   GRAPH_ELEMENT_TYPE_WF_COMMON_BASE,                 // Windows Forms базовый стандартный элемент управления
   GRAPH_ELEMENT_TYPE_WF_LABEL,                       // Windows Forms Label
   GRAPH_ELEMENT_TYPE_WF_BUTTON,                      // Windows Forms Button
   GRAPH_ELEMENT_TYPE_WF_CHECKBOX,                    // Windows Forms CheckBox
   GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,                 // Windows Forms RadioButton
   GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,            // Windows Forms CheckedListBox
  };
//+------------------------------------------------------------------+


В перечисление целочисленных свойств графического элемента впишем новые свойства, а общее количество целочисленных свойств увеличим с 81 до 83:

//+------------------------------------------------------------------+
//| Целочисленные свойства графического элемента на канвасе          |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Идентификатор элемента
   CANV_ELEMENT_PROP_TYPE,                            // Тип графического элемента
   //---...
   //---...
   CANV_ELEMENT_PROP_ACT_BOTTOM,                      // Нижняя граница активной зоны элемента
   CANV_ELEMENT_PROP_GROUP,                           // Группа, к которой принадлежит графический элемент
   CANV_ELEMENT_PROP_ZORDER,                          // Приоритет графического объекта на получение события нажатия мышки на графике
   //---...
   //---...
   CANV_ELEMENT_PROP_BUTTON_TOGGLE,                   // Флаг "Переключатель" элемента управления, имеющего кнопку
   CANV_ELEMENT_PROP_BUTTON_GROUP,                    // Флаг группы кнопки
   CANV_ELEMENT_PROP_BUTTON_STATE,                    // Состояние элемента управления "Переключатель", имеющего кнопку
   //---...
   //---...
   CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_DOWN,     // Цвет флажка проверки элемента управления при нажатии мышки на элемент управления
   CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_OVER,     // Цвет флажка проверки элемента управления при наведении мышки на элемент управления
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (83)          // Общее количество целочисленных свойств
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Количество неиспользуемых в сортировке целочисленных свойств
//+------------------------------------------------------------------+

Группа, к которой принадлежит графический элемент — это номер группы, а флаг группы кнопки — это флаг, указывающий на то, что кнопка работает в составе нескольких кнопок-переключателей. Если три кнопки составляют один объект-переключатель, то у каждой из них должен быть установлен флаг групповой кнопки, и при этом у них должна быть одна и та же группа.


Добавим новые свойства в перечисление возможных критериев сортировки графических элементов на канвасе:

//+------------------------------------------------------------------+
//| Возможные критерии сортировки графических элементов на канвасе   |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Сортировка по целочисленным свойствам
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Сортировать по идентификатору элемента
   SORT_BY_CANV_ELEMENT_TYPE,                         // Сортировать по типу графического элемента   
   //---...
   //---...
   SORT_BY_CANV_ELEMENT_ACT_BOTTOM,                   // Сортировать по нижней границе активной зоны элемента
   SORT_BY_CANV_ELEMENT_GROUP,                        // Сортировать по группе, к которой принадлежит графический элемент
   SORT_BY_CANV_ELEMENT_ZORDER,                       // Сортировать по приоритету графического объекта на получение события нажатия мышки на графике
   //---...
   //---...
   SORT_BY_CANV_ELEMENT_BUTTON_TOGGLE,                // Сортировать по флагу "Переключатель" элемента управления, имеющего кнопку
   SORT_BY_CANV_ELEMENT_BUTTON_GROUP,                 // Сортировать по флагу группы кнопки
   SORT_BY_CANV_ELEMENT_BUTTON_STATE,                 // Сортировать по состоянию элемента управления "Переключатель", имеющего кнопку
   SORT_BY_CANV_ELEMENT_CHECK_BACKGROUND_COLOR,       // Сортировать по цвету фона флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_BACKGROUND_COLOR_OPACITY,   // Сортировать по непрозрачности цвета фона флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_BACKGROUND_COLOR_MOUSE_DOWN,// Сортировать по цвету фона флажка проверки элемента управления при нажатии мышки на элемент управления
   SORT_BY_CANV_ELEMENT_CHECK_BACKGROUND_COLOR_MOUSE_OVER,// Сортировать по цвету фона флажка проверки элемента управления при наведении мышки на элемент управления
   SORT_BY_CANV_ELEMENT_CHECK_FORE_COLOR,             // Сортировать по цвету рамки флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_FORE_COLOR_OPACITY,     // Сортировать по непрозрачности цвета рамки флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_FORE_COLOR_MOUSE_DOWN,  // Сортировать по цвету рамки флажка проверки элемента управления при нажатии мышки на элемент управления
   SORT_BY_CANV_ELEMENT_CHECK_FORE_COLOR_MOUSE_OVER,  // Сортировать по цвету рамки флажка проверки элемента управления при наведении мышки на элемент управления
   SORT_BY_CANV_ELEMENT_CHECK_FLAG_COLOR,             // Сортировать по цвету флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_FLAG_COLOR_OPACITY,     // Сортировать по непрозрачности цвета флажка проверки элемента управления
   SORT_BY_CANV_ELEMENT_CHECK_FLAG_COLOR_MOUSE_DOWN,  // Сортировать по цвету флажка проверки элемента управления при нажатии мышки на элемент управления
   SORT_BY_CANV_ELEMENT_CHECK_FLAG_COLOR_MOUSE_OVER,  // Сортировать по цвету флажка проверки элемента управления при наведении мышки на элемент управления
//--- Сортировка по вещественным свойствам

//--- Сортировка по строковым свойствам
   SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Сортировать по имени объекта-элемента
   SORT_BY_CANV_ELEMENT_NAME_RES,                     // Сортировать по имени графического ресурса
   SORT_BY_CANV_ELEMENT_TEXT,                         // Сортировать по тексту графического элемента
  };
//+------------------------------------------------------------------+

Теперь мы сможем фильтровать, сортировать и выбирать графические элементы по новым свойствам.


В файле \MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:

//--- CPanel
   MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ,   // Не удалось создать объект-подложку
   MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE,           // Ошибка. Создаваемый объект должен иметь тип WinForms Base или быть его наследником

//--- CCheckedListBox
   MSG_CHECKED_LIST_ERR_FAILED_CREATE_CHECK_BOX_OBJ,  // Не удалось создать объект CheckBox
   MSG_CHECKED_LIST_ERR_FAILED_GET_CHECK_BOX_OBJ,     // Не удалось получить объект CheckBox из списка объектов

//--- Целочисленные свойства графических элементов

...

   MSG_CANV_ELEMENT_PROP_AUTOCHECK,                   // Автоматическое изменение состояния флажка при его выборе
   MSG_CANV_ELEMENT_PROP_BUTTON_TOGGLE,               // Флаг "Переключатель" элемента управления, имеющего кнопку
   MSG_CANV_ELEMENT_PROP_BUTTON_GROUP,                // Флаг группы кнопки
   MSG_CANV_ELEMENT_PROP_BUTTON_STATE,                // Состояние элемента управления "Переключатель", имеющего кнопку
   MSG_CANV_ELEMENT_PROP_CHECK_BACKGROUND_COLOR,      // Цвет фона флажка проверки элемента управления

и текстовые сообщения, соответствующие вновь добавленным индексам:

//--- CPanel
   {"Не удалось создать объект-подложку","Failed to create underlay object"},
   {"Ошибка. Создаваемый объект должен иметь тип WinForms Base или быть его наследником","Error. The object being created must be of type WinForms Base or be derived from it"},

//--- CCheckedListBox
   {"Не удалось создать объект CheckBox","Failed to create CheckBox object"},
   {"Не удалось получить объект CheckBox из списка объектов","Failed to get CheckBox object from list of objects"},

//--- Целочисленные свойства графических элементов

...

   {"Автоматическое изменение состояния флажка при его выборе","Automatically change the state of the checkbox when it is selected"},
   {"Флаг \"Переключатель\" элемента управления, имеющего кнопку","\"Button-toggle\" flag of a control"},
   {"Флаг группы кнопки","Button group flag"},
   {"Состояние элемента управления \"Переключатель\", имеющего кнопку","The \"Toggle-button\" control state"},
   {"Цвет фона флажка проверки элемента управления","The background color of the control's validation checkbox"},


В файле класса объекта-аккаунта \MQL5\Include\DoEasy\Objects\Accounts\Account.mqh в конструкторе класса немного поменяем определение типа сервера. Как уже говорилось выше, мы будем не получать строку имени терминала, а в строке с наименованием терминала искать подстроку "MetaTrader 5":

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CAccount::CAccount(void)
  {
   this.m_type=OBJECT_DE_TYPE_ACCOUNT;
//--- Инициализация контролируемых данных
   this.SetControlDataArraySizeLong(ACCOUNT_PROP_INTEGER_TOTAL);
   this.SetControlDataArraySizeDouble(ACCOUNT_PROP_DOUBLE_TOTAL);
   this.ResetChangesParams();
   this.ResetControlsParams();
  
//--- Сохранение целочисленных свойств
   this.m_long_prop[ACCOUNT_PROP_LOGIN]                              = ::AccountInfoInteger(ACCOUNT_LOGIN);
   this.m_long_prop[ACCOUNT_PROP_TRADE_MODE]                         = ::AccountInfoInteger(ACCOUNT_TRADE_MODE);
   this.m_long_prop[ACCOUNT_PROP_LEVERAGE]                           = ::AccountInfoInteger(ACCOUNT_LEVERAGE);
   this.m_long_prop[ACCOUNT_PROP_LIMIT_ORDERS]                       = ::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);
   this.m_long_prop[ACCOUNT_PROP_MARGIN_SO_MODE]                     = ::AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);
   this.m_long_prop[ACCOUNT_PROP_TRADE_ALLOWED]                      = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);
   this.m_long_prop[ACCOUNT_PROP_TRADE_EXPERT]                       = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT);
   this.m_long_prop[ACCOUNT_PROP_MARGIN_MODE]                        = #ifdef __MQL5__::AccountInfoInteger(ACCOUNT_MARGIN_MODE) #else ACCOUNT_MARGIN_MODE_RETAIL_HEDGING #endif ;
   this.m_long_prop[ACCOUNT_PROP_CURRENCY_DIGITS]                    = #ifdef __MQL5__::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) #else 2 #endif ;
   this.m_long_prop[ACCOUNT_PROP_SERVER_TYPE]                        = (::StringFind(::TerminalInfoString(TERMINAL_NAME),"MetaTrader 5")>WRONG_VALUE ? 5 : 4);
   this.m_long_prop[ACCOUNT_PROP_FIFO_CLOSE]                         = (#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<2155 ? false : ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE) #else false #endif );
   this.m_long_prop[ACCOUNT_PROP_HEDGE_ALLOWED]                      = (#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<3245 ? false : ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED) #else false #endif );
   
//--- Сохранение вещественных свойств
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_BALANCE)]          = ::AccountInfoDouble(ACCOUNT_BALANCE);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_CREDIT)]           = ::AccountInfoDouble(ACCOUNT_CREDIT);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_PROFIT)]           = ::AccountInfoDouble(ACCOUNT_PROFIT);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_EQUITY)]           = ::AccountInfoDouble(ACCOUNT_EQUITY);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN)]           = ::AccountInfoDouble(ACCOUNT_MARGIN);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_FREE)]      = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_LEVEL)]     = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_SO_CALL)]   = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_SO_SO)]     = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_INITIAL)]   = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_MAINTENANCE)]=::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_ASSETS)]           = ::AccountInfoDouble(ACCOUNT_ASSETS);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_LIABILITIES)]      = ::AccountInfoDouble(ACCOUNT_LIABILITIES);
   this.m_double_prop[this.IndexProp(ACCOUNT_PROP_COMMISSION_BLOCKED)]=::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED);
   
//--- Сохранение строковых свойств
   this.m_string_prop[this.IndexProp(ACCOUNT_PROP_NAME)]             = ::AccountInfoString(ACCOUNT_NAME);
   this.m_string_prop[this.IndexProp(ACCOUNT_PROP_SERVER)]           = ::AccountInfoString(ACCOUNT_SERVER);
   this.m_string_prop[this.IndexProp(ACCOUNT_PROP_CURRENCY)]         = ::AccountInfoString(ACCOUNT_CURRENCY);
   this.m_string_prop[this.IndexProp(ACCOUNT_PROP_COMPANY)]          = ::AccountInfoString(ACCOUNT_COMPANY);

//--- Имя объекта-аккаунта, тип объекта и тип аккаунта (MetaTrader 5 или 4)
   this.m_name=CMessage::Text(MSG_LIB_PROP_ACCOUNT)+" "+(string)this.Login()+": "+this.Name()+" ("+this.Company()+")";
   this.m_type=COLLECTION_ACCOUNT_ID;
   this.m_type_server=(uchar)this.m_long_prop[ACCOUNT_PROP_SERVER_TYPE];

//--- Заполнение текущих данных аккаунта
   for(int i=0;i<ACCOUNT_PROP_INTEGER_TOTAL;i++)
      this.m_long_prop_event[i][3]=this.m_long_prop[i];
   for(int i=0;i<ACCOUNT_PROP_DOUBLE_TOTAL;i++)
      this.m_double_prop_event[i][3]=this.m_double_prop[i];

//--- Обновление данных в базовом объекте и поиск изменений
   CBaseObjExt::Refresh();
  }
//+-------------------------------------------------------------------+

В переменную m_type_server впишем полученное выше значение (5 или 4). Ранее мы в неё записывали результат проверки имени терминала на значение "MetaTrader 5", что иногда бывает ошибочным. Теперь же мы в неё записываем то значение, которое уже записали в свойство аккаунта.


В файле \MQL5\Include\DoEasy\Objects\Graph\GBaseObj.mqh класса базового графического объекта библиотеки сделаем методы для установки и получения группы объекта виртуальными:

//--- Установка значений переменных класса
   void              SetObjectID(const long value)             { this.m_object_id=value;              }
   void              SetBelong(const ENUM_GRAPH_OBJ_BELONG belong){ this.m_belong=belong;             }
   void              SetTypeGraphObject(const ENUM_OBJECT obj) { this.m_type_graph_obj=obj;           }
   void              SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type) { this.m_type_element=type;   }
   void              SetSpecies(const ENUM_GRAPH_OBJ_SPECIES species){ this.m_species=species;        }
   virtual void      SetGroup(const int value)                 { this.m_group=value;                  }
   void              SetName(const string name)                { this.m_name=name;                    }
   void              SetDigits(const int value)                { this.m_digits=value;                 }

...

   virtual long      Zorder(void)                        const { return this.m_zorder;             }
   int               SubWindow(void)                     const { return this.m_subwindow;          }
   int               ShiftY(void)                        const { return this.m_shift_y;            }
   int               VisibleOnTimeframes(void)           const { return this.m_timeframes_visible; }
   int               Digits(void)                        const { return this.m_digits;             }
   virtual int       Group(void)                         const { return this.m_group;              }
   bool              IsBack(void)                        const { return this.m_back;               }
   bool              IsSelected(void)                    const { return this.m_selected;           }
   bool              IsSelectable(void)                  const { return this.m_selectable;         }
   bool              IsHidden(void)                      const { return this.m_hidden;             }
   bool              IsVisible(void)                     const { return this.m_visible;            }

Нам их потребуется переопределять в наследуемых классах.


Графические элементы в составе GUI взаимосвязаны общей иерархией их расположения относительно друг друга. Базовым объектом цепочки связанных объектов служит тот объект, к которому привязаны подчинённые. В свою очередь у подчинённых объектов могут быть свои цепочки связанных с ним объектов, а базовый в свою очередь — быть звеном цепочки объектов, привязанных к другому. При этом главным объектом считается тот, который имеет в своём составе подчинённые объекты, но сам ни к одному не привязан — он является родоначальником всей иерархии связей всех подчинённых объектов. Обычно таким объектом выступает окно в Windows, форма в C#, и здесь это тоже будет "Окно", так как определение "Форма" уже занято под графический элемент, в котором реализован функционал для работы с мышкой, и этот объект является роительским для базового WinForms-объекта.

Чтобы можно было получить идентификаторы базового и главного объекта, в файле  MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh класса графического элемента напишем методы, их возвращающие:

//--- Возвращает флаг, что объект является (1) главным, (2) базовым
   bool              IsMain(void)                                                      { return this.GetMain()==NULL;               }
   bool              IsBase(void)                                                      { return this.GetBase()==NULL;               }
//--- Возвращает идентификатор (1) главного, (2) базового объекта
   int               GetMainID(void)
                       {
                        if(this.IsMain())
                           return this.ID();
                        CGCnvElement *main=this.GetMain();
                        return(main!=NULL ? main.ID() : WRONG_VALUE);
                       }
   int               GetBaseID(void)
                       {
                        if(this.IsBase())
                           return this.ID();
                        CGCnvElement *base=this.GetBase();
                        return(base!=NULL ? base.ID() : WRONG_VALUE);
                       }
//--- Возвращает указатель на объект-канвас

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


Напишем виртуальные методы для получения и установки группы графического элемента:

//--- Приоритет графического объекта на получение события нажатия мышки на графике
   virtual long      Zorder(void)                        const { return this.GetProperty(CANV_ELEMENT_PROP_ZORDER);                    }
   virtual bool      SetZorder(const long value,const bool only_prop)
                       {
                        if(!CGBaseObj::SetZorder(value,only_prop))
                           return false;
                        this.SetProperty(CANV_ELEMENT_PROP_ZORDER,value);
                        return true;
                       }
//--- Группа графического объекта
   virtual int       Group(void)                         const { return (int)this.GetProperty(CANV_ELEMENT_PROP_GROUP);                }
   virtual void      SetGroup(const int value)
                       {
                        CGBaseObj::SetGroup(value);
                        this.SetProperty(CANV_ELEMENT_PROP_GROUP,value);
                       }
//+------------------------------------------------------------------+

Метод Group() возвращает значение, записанное в свойстве "группа" объекта.

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


Каждый графический элемент в коллекции должен иметь свой уникальный идентификатор. Это позволит нам обратиться напрямую к объекту по его идентификатору (если он запомнен в программе), а не искать его в циклах по всем объектам. На текущий момент уникальные идентификаторы присваиваются только тем графическим элементам, которые создаются непосредственно из программы. Если же создавать (тоже программно) новые привязанные объекты из уже созданных (которые привязываются к тому объекту, из которого идёт создание нового), то вновь созданному подчинённому объекту присваивается идентификатор того объекта, из которого он создавался. В этой ситуации мы можем получить этот объект по идентификатору базового с указанием номера объекта в списке подчинённых.

Конечно, такой подход тоже работает, но для упрощения работы с программами, создаваемыми на основе библиотеки, мы сделаем поиск уникального идентификатора и присвоение его вновь создаваемому подчинённому графическому элементу. Таким образом, у нас однозначно каждому графическому элементу будет назначаться уникальный идентификатор, по которому мы сможем к нему обратиться, и во-вторых, у нас останется тот способ, который реализован на данный момент — обратиться к базовому объекту и получить требуемый элемент из его списка по номеру элемента. Иметь два способа получения указателей на объекты однозначно лучше, чем один, тем более что теперь будет более быстрый способ обращения к элементу по его уникальному идентификатору.

В файле класса объекта-формы \MQL5\Include\DoEasy\Objects\Graph\Form.mqh в публичной секции объявим четыре метода для поиска максимального значения свойства объекта и идентификатора:

//--- Возвращает (1) себя, (2) список присоединённых объектов, (3) список объектов взаимодействия, (4) объект тени
   CForm            *GetObject(void)                                          { return &this;                  }
   CArrayObj        *GetListElements(void)                                    { return &this.m_list_elements;  }
   CArrayObj        *GetListInteractObj(void)                                 { return &this.m_list_interact;  }
   CShadowObj       *GetShadowObj(void)                                       { return this.m_shadow_obj;      }
//--- Возвращает максимальное значение  (1) указанного целочисленного свойства, (2) идентификатора из всех прикреплённых объектов к базовому
   long              GetMaxLongPropForm(const ENUM_CANV_ELEMENT_PROP_INTEGER prop);
   int               GetMaxIDForm(void);
//--- Возвращает максимальное значение (1) указанного целочисленного свойства, (2) идентификатора из всей иерархии связанных объектов
   long              GetMaxLongPropAll(const ENUM_CANV_ELEMENT_PROP_INTEGER prop);
   int               GetMaxIDAll(void);
//--- Возвращает указатель на (1) объект анимаций, список (2) текстовых, (3) прямоугольных кадров анимаций

Методы с параметрами будут возвращать найденное максимальное значение указанного свойства, тогда как методы без параметров будут возвращать найденное максимальное значение именно идентификатора графического элемента.


В методе для создания нового присоединённого элемента и добавления его в список присоединённых объектов CreateAndAddNewElement() поменяем установку идентификатора вновь созданного объекта.

Ранее здесь устанавливался идентификатор того объекта, из которого был создан новый:

   obj.SetID(this.ID());

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

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//| и добавляет его в список присоединённых объектов                 |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            const int x,
                                            const int y,
                                            const int w,
                                            const int h,
                                            const color colour,
                                            const uchar opacity,
                                            const bool activity)
  {
//--- Если тип создаваемого графического элемента меньше, чем "элемент" - сообщаем об этом и возвращаем false
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19));
      return NULL;
     }
//--- ...
//--- ...

//--- ...
//--- Устанавливаем минимальные свойства привязанному графическому элементу
   obj.SetBackgroundColor(colour,true);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
   obj.SetID(this.GetMaxIDAll()+1);
   obj.SetNumber(num);
   obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
   obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(obj.CoordXRelative());
   obj.SetCoordYRelativeInit(obj.CoordYRelative());
   return obj;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CForm::OnMouseEventPostProcessing(void)
  {
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {
      //--- Курсор за пределами формы, кнопки мышки не нажаты
      //--- Курсор за пределами формы, нажата кнопка мышки (любая)
      //--- Курсор за пределами формы, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED        :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED            :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL              :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED)
          {
           this.SetBackgroundColor(this.BackgroundColorInit(),false);
           this.SetBorderColor(this.BorderColorInit(),false);
           this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           this.Redraw(true);
          }
        break;
      //--- Курсор в пределах формы, кнопки мышки не нажаты
      //--- Курсор в пределах формы, нажата кнопка мышки (любая)
      //--- Курсор в пределах формы, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, кнопки мышки не нажаты
      //--- Курсор в пределах активной области, нажата кнопка мышки (любая)
      //--- Курсор в пределах активной области, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, отжата кнопка мышки (левая)
      //--- Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
      //--- Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
      //--- Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED         :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED             :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL               :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL        :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED     :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL        :
        break;
      //---MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий максимальное значение указанного целочисленного свойства из всех объектов, подчинённых базовому:

//+------------------------------------------------------------------+
//| Возвращает максимальное значение указанного целочисленного       |
//| свойства из всех подчинённых базовому объектов                   |
//+------------------------------------------------------------------+
long CForm::GetMaxLongPropForm(const ENUM_CANV_ELEMENT_PROP_INTEGER prop)
  {
//--- Получаем указатель на базовый объект
   CForm *base=this.GetBase();
//--- Если базовый объект получен, то устанавливаем в property значение указанного свойства, иначе - значение равно -1
   long property=(base!=NULL ? base.GetProperty(prop) : WRONG_VALUE);
//--- Если полученное значение больше -1
   if(property>WRONG_VALUE)
     {
      //--- В цикле по списку привязанных объектов
      for(int i=0;i<this.ElementsTotal();i++)
        {
         //--- получаем очередной объект
         CForm *elm=this.GetElement(i);
         if(elm==NULL)
            continue;
         //--- Если значение свойства полученного объекта больше значения, записанного в property -
         //--- устанавливаем в property значение свойства текущего объект
         if(elm.GetProperty(prop)>property)
            property=elm.GetProperty(prop);
         //--- Получаем максимальное значение свойства из привязанных объектов к текущему
         long prop_form=elm.GetMaxLongPropForm(prop);
         //--- Если полученное значение больше значения в property
         //--- устанавливаем в property полученное значение
         if(prop_form>property)
            property=prop_form;
        }
     }
//--- Возвращаем найденное максимальное значение свойства
   return property;
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий максимальное значение идентификатора графического элемента из всех объектов, прикреплённых к базовому:

//+------------------------------------------------------------------+
//| Возвращает максимальное значение идентификатора                  |
//| из всех прикреплённых объектов                                   |
//+------------------------------------------------------------------+
int CForm::GetMaxIDForm(void)
  {
   return (int)this.GetMaxLongPropForm(CANV_ELEMENT_PROP_ID);
  }
//+------------------------------------------------------------------+

Метод просто возвращает результат работы вышерассмотренного метода, в который передаётся для поиска именно свойство "идентификатор объекта".


Метод, возвращающий максимальное значение указанного целочисленного свойства из всей иерархии связанных объектов:

//+------------------------------------------------------------------+
//| Возвращает максимальное значение указанного целочисленного       |
//| свойства из всей иерархии связанных объектов                     |
//+------------------------------------------------------------------+
long CForm::GetMaxLongPropAll(const ENUM_CANV_ELEMENT_PROP_INTEGER prop)
  {
//--- Получаем указатель на главный объект
   CForm *main=(this.IsMain() ? this.GetObject() : this.GetMain());
//--- Если главный объект получен, то устанавливаем в property значение указанного свойства, иначе - значение равно -1
   long property=(main!=NULL ? main.GetProperty(prop) : WRONG_VALUE);
//--- Если полученное значение больше -1
   if(property>WRONG_VALUE)
     {
      //--- В цикле по списку привязанных объектов к главному объекту
      for(int i=0;i<main.ElementsTotal();i++)
        {
         //--- получаем очередной объект
         CForm *elm=main.GetElement(i);
         if(elm==NULL)
            continue;
         //--- Получаем максимальное значение свойства из всей иерархии объектов, подчинённых текущему в цикле
         long prop_form=elm.GetMaxLongPropForm(prop);
         //--- Если полученное значение больше значения в property
         //--- устанавливаем в property полученное значение
         if(prop_form>property)
            property=prop_form;
        }
     }
//--- Возвращаем найденное максимальное значение свойства
   return property;
  }
//+------------------------------------------------------------------+

Логика метода подробно расписана в комментариях к коду, и она аналогична методу поиска максимального свойства объектов, прикреплённых к базовому. В отличии от первого, в этом методе мы начинаем поиск от главного объекта — родоначальника иерархии связанных объектов и проходим по спискам всех объектов всей иерархической цепочки связанных объектов. В итоге имеем самое большое значение свойства из всей иерархии подчинённых объектов.


Метод, возвращающий максимальное значение идентификатора из всей иерархии связанных объектов:

//+------------------------------------------------------------------+
//| Возвращает максимальное значение идентификатора                  |
//| из всей иерархии связанных объектов                              |
//+------------------------------------------------------------------+
int CForm::GetMaxIDAll(void)
  {
   return (int)this.GetMaxLongPropAll(CANV_ELEMENT_PROP_ID);
  }
//+------------------------------------------------------------------+

Метод возвращает результат вызова вышерассмотренного метода, в параметрах которого передаётся свойство "идентификатор" для поиска его максимального значения.


Описания свойств графических элементов у нас реализованы в классе базового WinForms-объекта
в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh.

В методе GetPropertyDescription() впишем блоки кода для возврата описания двух новых свойств графического элемента:

      property==CANV_ELEMENT_PROP_ACT_BOTTOM                   ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ACT_BOTTOM)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_GROUP                        ?  CMessage::Text(MSG_GRAPH_OBJ_PROP_GROUP)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_ZORDER                       ?  CMessage::Text(MSG_GRAPH_OBJ_PROP_ZORDER)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :

...

      property==CANV_ELEMENT_PROP_BUTTON_TOGGLE                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_BUTTON_TOGGLE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)(bool)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_BUTTON_GROUP                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_BUTTON_GROUP)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)(bool)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_BUTTON_STATE                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_BUTTON_STATE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)(bool)this.GetProperty(property)
         )  :

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


WinForms-объект RadioButton может правильно работать только в связке с другими объектами этого типа. Кроме того, эти объекты должны либо быть привязанными к одному и тому же контейнеру, либо иметь одинаковое значение группы — входить в одну группу объектов. Если по одному из таких объектов щёлкнуть мышкой, то его флажок будет выбран (если до этого выбранным не был), а у всех остальных объектов этой группы флажки выбора будут сняты. При повторном щелчке по уже выбранному объекту его флажок не снимается.

Внесём доработки в класс объекта RadioButton в файле\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\RadioButton.mqh.

В приватной секции класса объявим метод, устанавливающий всем объектам одной группы состояние "не выбрано", а в публичной секции напишем метод, устанавливающий указанное состояние объекта и его флажка выбора:

//+------------------------------------------------------------------+
//| Класс объекта CheckBox элементов управления WForms               |
//+------------------------------------------------------------------+
class CRadioButton : public CCheckBox
  {
private:
//--- Устанавливает состояние флажка проверки как "невыбрано" для всех RadioButton одной группы в контейнере
   void              UncheckOtherAll(void);
protected:
//--- Отображает флажок проверки по указанному состоянию
   virtual void      ShowControlFlag(const ENUM_CANV_ELEMENT_CHEK_STATE state);

//--- Обработчик события  Курсор в пределах активной области, отжата кнопка мышки (левая)
   virtual void      MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
   
public:
//--- Устанавливает состояние флажка проверки
   virtual void      SetChecked(const bool flag)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_CHECKED,flag);
                        this.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)flag);
                        if(this.Checked())
                           this.UncheckOtherAll();
                       }
//--- Конструктор

В методе установки состояния флажка проверки записываем переданное в метод значение в его свойство, затем устанавливаем состояние выбора (либо выбрано, либо нет). И далее, если состояние объекта "выбран", то вызываем метод, в котором всем остальным аналогичным объектам этой группы устанавливается состояние "не выбран" и снимается флажок выбора.


В обработчике события "Курсор в пределах активной области, отжата кнопка мышки (левая)" допишем блок кода, в котором проверяется состояние объекта,  если он в невыбранном состоянии, то вызываем метод для установки объекта в состояние "выбран":

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CRadioButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если кнопка мышки отпущена за пределами элемента - это отказ от взаимодействия с элементом
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      this.SetCheckBackgroundColor(this.BackgroundColorInit(),false);
      this.SetCheckBorderColor(this.CheckBorderColorInit(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- Если кнопка мышки отпущена в пределах элемента - это щелчок по элементу управления
   else
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false);
      this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      if(!this.Checked())
         this.SetChecked(true);
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID(),", Group=",this.Group());
     }
   this.Redraw(false);
  }
//+------------------------------------------------------------------+

Здесь в журнал выводится отладочная запись с указанием события, состояния элемента (выбран/не выбран), его идентификатора и номера группы. Потом эту запись уберём и заменим на отправку сообщения библиотеке и управляющей программе.


Метод, устанавливающий состояние флажка проверки как "невыбрано" для всех объектов RadioButton одной группы в контейнере:

//+------------------------------------------------------------------+
//| Устанавливает состояние флажка проверки как "невыбрано"          |
//| для всех объектов RadioButton одной группы в контейнере          |
//+------------------------------------------------------------------+
void CRadioButton::UncheckOtherAll(void)
  {
//--- Получаем указатель на базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех объектов с типом RadioButton
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON);
//--- Из полученного списка выбираем все объекты, кроме данного (имена выбранных объектов не равны имени этого)
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL);
//--- Из полученного списка выбираем только те объекты, у которых номер группы совпадает с группой этого
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL);
//--- Если список объектов получен,
   if(list!=NULL)
     {
      //--- в цикле по всем объектам в списке
      for(int i=0;i<list.Total();i++)
        {
         //--- получаем очередной объект,
         CRadioButton *obj=list.At(i);
         if(obj==NULL)
            continue;
         //--- устанавливаем ему состояние "не выбран"
         obj.SetChecked(false);
         //--- Перерисовываем объект для отображения невыбранного флажка
         obj.Redraw(false);
        }
     }
  }
//+------------------------------------------------------------------+

Каждая строчка кода прокомментирована и, надеюсь, логика метода вопросов не вызовет. Вкратце: нам нужно получить список всех объектов RadioButton, прикреплённых к контейнеру. Они все должны иметь одинаковую группу и в списке не должно быть объекта, из которого вызван этот метод (ведь это тот объект, по которому был щелчок мышкой, и он стал выбранным, а значит — с него флажок выбора снимать не нужно). Проходим в цикле по полученному списку и устанавливаем каждому из объектов невыбранное состояние и снимаем флажок. Для отображения изменений объекты перерисовываются.


В классе объекта-кнопки в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh сделаем аналогичные доработки, что позволит сделать кнопки не просто "щёлкающими", а они смогут иметь состояние включено/выключено. Соответственно, мы их сможем определять в группы, в которых связанные одной группой кнопки будут работать совместно.

В приватной секции класса объявим метод, устанавливающий всем кнопкам одной группы состояние "отжата":

//+------------------------------------------------------------------+
//| Класс объекта Label элементов управления WForms                  |
//+------------------------------------------------------------------+
class CButton : public CLabel
  {
private:
   int               m_text_x;                                 // Координата X текста
   int               m_text_y;                                 // Координата Y текста
   color             m_array_colors_bg_tgl[];                  // Массив цветов фона элемента для состояния "включено"
   color             m_array_colors_bg_tgl_dwn[];              // Массив цветов фона элемента для состояния "включено" при нажатии мышки на элемент управления
   color             m_array_colors_bg_tgl_ovr[];              // Массив цветов фона элемента для состояния "включено" при наведении мышки на элемент управления
   color             m_array_colors_bg_tgl_init[];             // Массив изначальных цветов фона элемента для состояния "включено"
//--- Устанавливает состояние кнопки как "отжато" для всех Button одной группы в контейнере
   void              UnpressOtherAll(void);
protected:


В публичной секции класса изменим метод, устанавливающий состояние кнопки, и напишем методы для установки и возврата флага кнопки, работающей в группе с другими объектами-кнопками:

//--- (1) Устанавливает, (2) возвращает состояние элемента управления "Переключатель"
   void              SetState(const bool flag)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,flag);
                        if(this.State())
                          {
                           this.UnpressOtherAll();
                          }
                       }
   bool              State(void)                         const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_STATE);                                }
   
//--- (1) Устанавливает, (2) возвращает флаг группы
   void              SetGroupButtonFlag(const bool flag)       { this.SetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP,flag);                                        }
   bool              GroupButton(void)                   const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP);                                }

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


В обработчике последнего события мышки допишем проверку ещё одного условия аналогично одноимённому методу класса объекта-формы, рассмотренному нами выше:

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CButton::OnMouseEventPostProcessing(void)
  {
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {
      //--- Курсор за пределами формы, кнопки мышки не нажаты
      //--- Курсор за пределами формы, нажата кнопка мышки (любая)
      //--- Курсор за пределами формы, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED     :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL       :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED)
          {
           this.SetBackgroundColor(this.State() ? this.BackgroundColorToggleON() : this.BackgroundColorInit(),false);
           this.SetBorderColor(this.BorderColorInit(),false);
           this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           this.Redraw(false);
          }
        break;
      //--- Курсор в пределах формы, кнопки мышки не нажаты
      //--- Курсор в пределах формы, нажата кнопка мышки (любая)
      //--- Курсор в пределах формы, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, кнопки мышки не нажаты
      //--- Курсор в пределах активной области, нажата кнопка мышки (любая)
      //--- Курсор в пределах активной области, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, отжата кнопка мышки (левая)
      //--- Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
      //--- Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
      //--- Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL :
        break;
      //---MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+


Метод, устанавливающий состояние кнопки как "отжато" для всех объектов Button одной группы в контейнере:

//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки как "отжато"                      |
//| для всех Button одной группы в контейнере                        |
//+------------------------------------------------------------------+
void CButton::UnpressOtherAll(void)
  {
//--- Получаем указатель на базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех объектов с типом Button
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_BUTTON);
//--- Из полученного списка выбираем все объекты, кроме данного (имена выбранных объектов не равны имени этого)
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL);
//--- Из полученного списка выбираем только те объекты, у которых номер группы совпадает с группой этого
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL);
//--- Если список объектов получен,
   if(list!=NULL)
     {
      //--- в цикле по всем объектам в списке
      for(int i=0;i<list.Total();i++)
        {
         //--- получаем очередной объект,
         CButton *obj=list.At(i);
         if(obj==NULL)
            continue;
         //--- устанавливаем кнопке состояние "отжата",
         obj.SetState(false);
         //--- устанавливаем цвет фона изначальным (курсор находится на другой кнопке за пределами этой)
         obj.SetBackgroundColor(obj.BackgroundColorInit(),false);
         //--- Перерисовываем объект для отображения изменений
         obj.Redraw(false);
        }
     }
  }
//+------------------------------------------------------------------+

Логика метода идентична методу класса объекта RadioButton, полностью расписана в комментариях к коду, и в пояснениях, надеюсь, не нуждается.


WinForms-объект CheckBox в своём обычном состоянии при наведении на него курсора мышки меняет цвет фона, флажка и рамки области флажка. Фон самого объекта остаётся неизменным (прозрачным). Но если такие объекты объединены в группу (как это будет в объекте, который далее будем делать), то при наведении на объект курсора мышки, его цвет фона тоже изменяется. Чтобы нам использовать объект CheckBox для создания объекта-списка объектов CheckBox, внесём изменения в этот объект в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh.

Метод для установки состояния флажка проверки сделаем виртуальным, как и в его объекте-наследнике RadioButton:

//--- (1) Устанавливает, (2) возвращает состояние флажка проверки
   virtual void      SetChecked(const bool flag)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_CHECKED,flag);
                        if((bool)this.CheckState()!=flag)
                           this.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)flag);
                       }
   bool              Checked(void)                             const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_CHECKED);                            }


В конструкторе класса явно установим полную непрозрачность цвету фона объекта и зададим смещение активной зоны на один пиксель с каждой стороны:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(const long chart_id,
                     const int subwindow,
                     const string name,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CLabel(chart_id,subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX);
   CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CHECKBOX);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetCoordX(x);
   this.SetCoordY(y);
   this.SetCheckWidth(DEF_CHECK_SIZE);
   this.SetCheckHeight(DEF_CHECK_SIZE);
   this.SetWidth(w);
   this.SetHeight(h);
   this.Initialize();
   this.SetOpacity(0);
   this.SetForeColorMouseDown(CLR_DEF_FORE_COLOR_MOUSE_DOWN);
   this.SetForeColorMouseOver(CLR_DEF_FORE_COLOR_MOUSE_OVER);
   this.SetCheckBackgroundColor(CLR_DEF_CHECK_BACK_COLOR,true);
   this.SetCheckBackgroundColorMouseDown(CLR_DEF_CHECK_BACK_MOUSE_DOWN);
   this.SetCheckBackgroundColorMouseOver(CLR_DEF_CHECK_BACK_MOUSE_OVER);
   this.SetCheckBorderColor(CLR_DEF_CHECK_BORDER_COLOR,true);
   this.SetCheckBorderColorMouseDown(CLR_DEF_CHECK_BORDER_MOUSE_DOWN);
   this.SetCheckBorderColorMouseOver(CLR_DEF_CHECK_BORDER_MOUSE_OVER);
   this.SetCheckFlagColor(CLR_DEF_CHECK_FLAG_COLOR,true);
   this.SetCheckFlagColorMouseDown(CLR_DEF_CHECK_FLAG_MOUSE_DOWN);
   this.SetCheckFlagColorMouseOver(CLR_DEF_CHECK_FLAG_MOUSE_OVER);
   this.SetWidthInit(this.Width());
   this.SetHeightInit(this.Height());
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.SetTextAlign(ANCHOR_LEFT);
   this.SetActiveAreaShift(1,1,1,1);
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_check_x=0;
   this.m_check_y=0;
   this.Redraw(false);
  }
//+------------------------------------------------------------------+

Здесь мы устанавливаем полную непрозрачность фона по той причине, что нам потребуется далее создавать объекты с непрозрачностью как у базового объекта и, чтобы не задавать постоянно непрозрачность этому объекту в его обычном состоянии при его создании, здесь мы явно зададим её в конструкторе. А смещение активной области на один пиксель с каждой из сторон — это попытка увеличить зазор между рядом стоящими объектами CheckBox для того, чтобы при уводе курсора с одного объекта, он успевал "побывать" за пределами объекта, не наводясь сразу на следующий — чтобы курсор прошёл через базовый объект, а не попадал сразу на находящийся рядом. Всё это — результаты поиска решения проблемы, при которой расположенные рядом объекты не всегда восстанавливают свой цвет фона по умолчанию после увода курсора за его пределы. Впрочем, такое решение не всегда помогает, и всё же придётся найти время, чтобы досконально разобраться в этом вопросе и решить проблему.

В методе, перерисовывающем объект, вместо указания полной непрозрачности (значение 0) теперь будем указывать значение величины непрозрачности, записанное в свойствах объекта:

//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CCheckBox::Redraw(bool redraw)
  {
//--- Заполняем объект цветом фона с полной прозрачностью
   this.Erase(this.BackgroundColor(),this.Opacity(),true);
//--- Устанавливаем скорректированные координаты текста относительно флажка проверки
   this.SetCorrectTextCoords();
//--- Рисуем текст и флажок выбора в установленных координатах объекта и точкой привязки и обновляем объект 
   this.Text(this.m_text_x,this.m_text_y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.ShowControlFlag(this.CheckState());
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

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


Во всех обработчиках событий мышки, где требуется изменение цвета фона объекта, впишем соответствующие доработки:

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| кнопки мышки не нажаты                                           |
//+------------------------------------------------------------------+
void CCheckBox::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false);
   this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false);
   this.SetCheckFlagColor(this.CheckFlagColorMouseOver(),false);
   this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
   this.Redraw(false);
  }
//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| нажата кнопка мышки (любая)                                      |
//+------------------------------------------------------------------+
void CCheckBox::MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseDown(),false);
   this.SetCheckBorderColor(this.CheckBorderColorMouseDown(),false);
   this.SetCheckFlagColor(this.CheckFlagColorMouseDown(),false);
   this.SetBackgroundColor(this.BackgroundColorMouseDown(),false);
   this.Redraw(false);
  }
//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CCheckBox::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если кнопка мышки отпущена за пределами элемента - это отказ от взаимодействия с элементом
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false);
      this.SetCheckBorderColor(this.CheckBorderColorInit(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      this.SetBackgroundColor(this.BackgroundColorInit(),false);
      //--- Выведем в журнал тестовую запись
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- Если кнопка мышки отпущена в пределах элемента - это щелчок по элементу управления
   else
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false);
      this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
      this.SetChecked(!this.Checked());
      //--- Выведем в журнал тестовую запись
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID());
     }
   this.Redraw(false);
  }
//+------------------------------------------------------------------+


В обработчик последнего события мышки допишем проверку уже знакомого нам состояния:

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CCheckBox::OnMouseEventPostProcessing(void)
  {
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {
      //--- Курсор за пределами формы, кнопки мышки не нажаты
      //--- Курсор за пределами формы, нажата кнопка мышки (любая)
      //--- Курсор за пределами формы, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED     :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL       :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED)
          {
           this.SetBackgroundColor(this.BackgroundColorInit(),false);
           this.SetBorderColor(this.BorderColorInit(),false);
           this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false);
           this.SetCheckBorderColor(this.CheckBorderColorInit(),false);
           this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
           this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           this.Redraw(false);
          }
        break;
      //--- Курсор в пределах формы, кнопки мышки не нажаты
      //--- Курсор в пределах формы, нажата кнопка мышки (любая)
      //--- Курсор в пределах формы, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, кнопки мышки не нажаты
      //--- Курсор в пределах активной области, нажата кнопка мышки (любая)
      //--- Курсор в пределах активной области, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, отжата кнопка мышки (левая)
      //--- Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
      //--- Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
      //--- Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL :
        break;
      //---MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

Теперь можно приступать к созданию нового объекта.


WInForms-объект CheckedListBox

Данный WinForms-объект представляет из себя панель, на которой расположен список из объектов CheckBox. При наведении курсора мышки на объекты списка, их цвет фона меняется вместе с цветом флажка проверки, его фона и рамки области флажка. Объекты из списка могут располагаться как вертикально друг над другом, так и в колонки по несколько штук. Сегодня сделаем только вертикальное расположение объектов.

В каталоге библиотеки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ создадим новый файл CheckedListBox.mqh класса CCheckedListBox.

Класс должен быть унаследован от базового объекта-контейнера, а для того, чтобы он "видел" и класс CContainer, и класс CCheckBox, подключим к нему файл класса объекта-панели, в котором все нужные файлы нужных нам классов уже видны:

//+------------------------------------------------------------------+
//|                                               CheckedListBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| Класс объекта CCheckedListBox элементов управления WForms        |
//+------------------------------------------------------------------+
class CCheckedListBox : public CContainer
  {
  }


В приватной секции объявим виртуальный метод для создания нового графического объекта, а в публичной секции объявим метод для создания списка из указанного количества объектов CheckBox и параметрический конструктор:

//+------------------------------------------------------------------+
//| Класс объекта CCheckedListBox элементов управления WForms        |
//+------------------------------------------------------------------+
class CCheckedListBox : public CContainer
  {
private:
//--- Создаёт новый графический объект
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_num,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity);
public:
//--- Создаёт указанное количество объектов CheckBox
   void              CreateCheckBox(const int count);
//--- Конструктор
                     CCheckedListBox(const long chart_id,
                                     const int subwindow,
                                     const string name,
                                     const int x,
                                     const int y,
                                     const int w,
                                     const int h);
  };
//+------------------------------------------------------------------+


Параметрический конструктор:

//+------------------------------------------------------------------+
//| Конструктор с указанием идентификатора чарта и подокна           |
//+------------------------------------------------------------------+
CCheckedListBox::CCheckedListBox(const long chart_id,
                                 const int subwindow,
                                 const string name,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h) : CContainer(chart_id,subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX);
   CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
  }
//+------------------------------------------------------------------+

Устанавливаем объекту его тип WinForms-объекта и тип графического объекта библиотеки. Далее задаём размер рамки объекта в один пиксель, тип рамки — простая, и устанавливаем цвет рамки и текста по умолчанию для графических объектов библиотеки.


Метод, создающий указанное количество объектов CheckBox на основной панели:

//+------------------------------------------------------------------+
//| Создаёт указанное количество объектов CheckBox                   |
//+------------------------------------------------------------------+
void CCheckedListBox::CreateCheckBox(const int count)
  {
//--- Создаём указатель на объект CheckBox
   CCheckBox *cbox=NULL;
//--- В цикле по указанному количеству объектов
   for(int i=0;i<count;i++)
     {
      //--- Задаём координаты и размеры создаваемого объекта
      int x=this.BorderSizeLeft()+1;
      int y=(cbox==NULL ? this.BorderSizeTop()+1 : cbox.BottomEdgeRelative()+4);
      int w=this.Width()-this.BorderSizeLeft()-this.BorderSizeRight();
      int h=DEF_CHECK_SIZE+1;
      //--- Если объект создать не удалось - выводим об этом сообщение в журнал
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,x,y,w,h,clrNONE,255,true,false))
         CMessage::ToLog(DFUN,MSG_CHECKED_LIST_ERR_FAILED_CREATE_CHECK_BOX_OBJ);
      //--- Получаем созданный объект из списка по индексу цикла
      cbox=this.GetElement(i);
      //--- Если объект получить не удалось - выводим об этом сообщение в журнал
      if(cbox==NULL)
         CMessage::ToLog(DFUN,MSG_CHECKED_LIST_ERR_FAILED_GET_CHECK_BOX_OBJ);
      //--- Устанавливаем размер рамки в ноль
      cbox.SetBorderSizeAll(0);
      //--- Устанавливаем выравнивание флажка проверки и текста слева по центру
      cbox.SetCheckAlign(ANCHOR_LEFT);
      cbox.SetTextAlign(ANCHOR_LEFT);
      //--- Устанавливаем текст объекта
      cbox.SetText("CheckBox"+string(i+1));
      //--- Устанавливаем непрозрачность как у базового объекта и цвета фона по умолчанию
      cbox.SetOpacity(this.Opacity());
      cbox.SetBackgroundColor(this.BackgroundColor(),true);
      cbox.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_STD_MOUSE_OVER);
      cbox.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_STD_MOUSE_DOWN);
     }
//--- Для базового объекта устанавливаем режим автоизменения размера как "увеличить и уменьшить"
//--- и ставим флаг автоматического изменения размера
   this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false);
   this.SetAutoSize(true,false);
  }
//+------------------------------------------------------------------+

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

Виртуальный метод, создающий новый графический объект:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CCheckedListBox::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                                const int obj_num,
                                                const string obj_name,
                                                const int x,
                                                const int y,
                                                const int w,
                                                const int h,
                                                const color colour,
                                                const uchar opacity,
                                                const bool movable,
                                                const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
//--- создаём объект CheckBox
   CGCnvElement *element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
//--- установим объекту флаг перемещаемости и относительные координаты
   element.SetMovable(movable);
   element.SetCoordXRelative(element.CoordX()-this.CoordX());
   element.SetCoordYRelative(element.CoordY()-this.CoordY());
   return element;
  }
//+------------------------------------------------------------------+

Так как одним из родителей данного объекта является класс объекта-формы, где создан функционал для работы с мышкой, а в нём не виден класс этого объекта, то виртуальный метод класса CForm для создания нового графического объекта нам нужно переопределить здесь. В этом методе нам не нужно проверять тип объекта, передаваемый в метод, так как здесь мы точно знаем какой тип объекта нужно создать, и этот тип — CheckBox, который мы тут и создаём и устанавливаем для него минимальные значения — флаг перемещаемости и относительные координаты.

Все остальные методы для работы класса уже есть в его родительских классах.

Естественно, далее мы будем дорабатывать класс этого объекта для реализации его дополнительного функционала. Но сегодня для проверки остановимся на этом.

Теперь нам необходимо дописать обработку данного типа объекта во всех классах-контейнерах — чтобы мы могли в них создавать такие объекты.

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh класса объекта-панели подключим файл только что созданного класса:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "GroupBox.mqh"
#include "..\..\WForms\Common Controls\CheckedListBox.mqh"
//+------------------------------------------------------------------+
//| Класс объекта Panel элементов управления WForms                  |
//+------------------------------------------------------------------+

Теперь этот новый класс будет виден во всех классах-контейнерах библиотеки.


В методе, создающем новый графический объект, добавим обработку типа нового объекта библиотеки:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       const int obj_num,
                                       const string obj_name,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h,
                                       const color colour,
                                       const uchar opacity,
                                       const bool movable,
                                       const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER         :
         element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX          :
         element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL             :
         element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
         element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
         element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
         element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
         element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
         element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+

Здесь мы просто создаём новый объект класса CCheckedListBox.

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh класса базового объекта-контейнера в методе, создающем новый присоединённый элемент, установим созданному объекту группу как у базового объекта, но только в том случае, если созданный объект не является объектом-контейнером. Для WinForms-объектов "Текстовая метка", "CheckBox", "RadioButton" добавим установку прозрачного цвета фона и полную его прозрачность и добавим обработку WinForms-объекта "CheckedListBox":

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CContainer::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h,
                                  const color colour,
                                  const uchar opacity,
                                  const bool activity,
                                  const bool redraw)
  {
//--- Если тип объекта - меньше, чем базовый WinForms-объект
   if(element_type<GRAPH_ELEMENT_TYPE_WF_BASE)
     {
      //--- сообщаем об ошибке и возвращаем false
      CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE);
      return false;
     }
//--- Если не удалось создать новый графический элемент - возвращаем false
   CWinFormBase *obj=CForm::CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
   if(obj==NULL)
      return false;
//--- Устанавливаем созданному объекту цвет текста как у базового контейнера
   obj.SetForeColor(this.ForeColor(),true);
//--- Если созданный объект не является контейнером - устанавливаем для него такую же группу, как у его базового объекта
   if(obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_CONTAINER || obj.TypeGraphElement()>GRAPH_ELEMENT_TYPE_WF_GROUPBOX)
      obj.SetGroup(this.Group());
//--- В зависимости от типа созданного объекта
   switch(obj.TypeGraphElement())
     {
      //--- Для WinForms-объектов "Контейнер", "Панель", "GroupBox"
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER         :
      case GRAPH_ELEMENT_TYPE_WF_PANEL             :
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX          :
        //--- устанавливаем цвет рамки равным цвету фона 
        obj.SetBorderColor(obj.BackgroundColor(),true);
        break;
      //--- Для WinForms-объектов "Текстовая метка", "CheckBox", "RadioButton"
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
        //--- устанавливаем цвет текста объекта в зависимости от переданного в метод:
        //--- либо цвет текста контейнера, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        //--- Цвет фона устанавливаем прозрачным
        obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetOpacity(0,false);
        break;
      //--- Для WinForms-объекта "Button"
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
        //--- устанавливаем цвет текста объекта как цвет текста контейнера в зависимости от переданного в метод:
        //--- цвет фона устанавливаем  в зависимости от переданного в метод:
        //--- либо цвет фона стандартных элементов управления по умолчанию, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        obj.SetForeColor(this.ForeColor(),true);
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
        break;
      //--- Для WinForms-объекта "CheckedListBox"
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
        //--- устанавливаем цвет текста объекта как цвет текста контейнера в зависимости от переданного в метод:
        //--- цвет фона устанавливаем  в зависимости от переданного в метод:
        //--- либо цвет фона стандартных элементов управления по умолчанию, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
        obj.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
        obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
        break;
      default:
        break;
     }
//--- Если у панели включено автоизменение размеров и есть привязанные объекты - вызываем метод изменения размеров
   if(this.AutoSize() && this.ElementsTotal()>0)
      this.AutoSizeProcess(redraw);
//--- Перерисовываем панель и все добавленные объекты и возвращаем true
   this.Redraw(redraw);
   return true;
  }
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh класса объекта GroupBox в методе для создания нового графического объекта добавим обработку нового типа объекта CheckedListBox:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CGroupBox::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int obj_num,
                                          const string obj_name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT              :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM                 :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER         :
         element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX          :
         element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL             :
         element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
         element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
         element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
         element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
         element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
         element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+


В методе инициализации переменных Initialize() в самом конце добавим установку значения группы по умолчанию:

   this.SetEnabled(true);
   this.SetVisible(true,false);
//--- Установим значние группы по умолчанию
   this.SetGroup((int)this.GetMaxLongPropAll(CANV_ELEMENT_PROP_GROUP)+1);
  }
//+------------------------------------------------------------------+

Здесь мы вызываем ранее рассмотренный нами метод, возвращающий максимальное значение указанного свойства из всех объектов в коллекции графических элементов библиотеки. Искомым параметром указываем свойство "группа", а к полученному значению прибавляем 1, что сделает значение новой группы максимальным. Для объектов-контейнеров важно иметь уникальную группу, отличающую её от остальных объектов-контейнеров.


В файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh класса-коллекции графических элементов в список включаемых файлов добавим файл класса объекта CheckedListBox:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Services\Select.mqh"
#include "..\Objects\Graph\WForms\Containers\GroupBox.mqh"
#include "..\Objects\Graph\WForms\Containers\Panel.mqh"
#include "..\Objects\Graph\WForms\Common Controls\CheckedListBox.mqh"
#include "..\Objects\Graph\Standard\GStdVLineObj.mqh"
#include "..\Objects\Graph\Standard\GStdHLineObj.mqh"


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

//--- Возвращает экранные координаты каждой опорной точки графического объекта
   bool              GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]);
//--- Возвращает максимальный идентификатор из всех графических элементов коллекции
   int               GetMaxID(void);
   
public:


Во всех методах создания графических элементов заменим строку, где в качестве идентификатора устанавливается общее количество графических элементов в коллекции,

int id=this.m_list_all_canv_elm_obj.Total();

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

//--- Создаёт объект-графический элемент на канвасе на указанном графике и подокне
   int               CreateElement(const long chart_id,
                                   const int subwindow,
                                   const string name,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   const color clr,
                                   const uchar opacity,
                                   const bool movable,
                                   const bool activity,
                                   const bool redraw=false)
                       {
                        int id=this.GetMaxID()+1;
                        CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,id,0,chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,activity,redraw);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        if(res==ADD_OBJ_RET_CODE_EXIST)
                           obj.SetID(id);
                        obj.Erase(clr,opacity,redraw);
                        return obj.ID();
                       }
//--- Создаёт объект-графический элемент на канвасе на указанном графике и подокне с заливкой вертикальным градиентом

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

В методе обработки бывшей активной формы под курсором добавим вызов обработчика события мышки для текущего объекта цикла — чтобы не пропускать его обработку в случае, если объект при уводе курсора с него, ещё не попал в список неактивных объектов, в которых необходимо поменять цвет на исходный, но уже является таковым:

//+------------------------------------------------------------------+
//| Постобработка бывшей активной формы под курсором                 |
//+------------------------------------------------------------------+
void CGraphElementsCollection::FormPostProcessing(void)
  {
 //--- Получаем все элементы с типом CForm и выше
   CArrayObj *list=GetList(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_FORM,EQUAL_OR_MORE);
   if(list==NULL)
      return;
   //--- В цикле по списку полученных элементов
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем указатель на объект
      CForm *obj=list.At(i);
      //--- если указатель получить не удалось - идём к следующему объекту в списке
      if(obj==NULL)
         continue;
      obj.OnMouseEventPostProcessing();
      //--- Создаём список объектов взаимодействия объекта и получаем их количество
      int count=obj.CreateListInteractObj();
      //--- В цикле по полученному списку
      for(int j=0;j<count;j++)
        {
         //--- получаем очередной объект
         CForm *elm=obj.GetInteractForm(j);
         if(elm==NULL)
            continue;
         //--- и вызываем его метод обработки событий мышки
         elm.OnMouseEventPostProcessing();
        }
     }
  }
//+------------------------------------------------------------------+


Для того, чтобы в будущем создать обработку взаимодействия правой кнопки мышки с объектами графического интерфейса программы, в обработчике событий OnChartEvent() добавим проверку нажатия и удержания правой кнопки мышки:

//--- Обработка событий мышки графических объектов на канвасе
//--- Если событие - не изменение графика
   else
     {
      //--- Проверим, нажата ли кнопка мышки
      ENUM_MOUSE_BUTT_KEY_STATE butt_state=this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam);
      bool pressed=(butt_state==MOUSE_BUTT_KEY_STATE_LEFT || butt_state==MOUSE_BUTT_KEY_STATE_RIGHT ? true : false);
      ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE;
      //--- Объявим статические переменные для активной формы и флагов состояний

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


Метод, возвращающий максимальный идентификатор из всех графических элементов коллекции:

//+------------------------------------------------------------------+
//| Возвращает максимальный идентификатор                            |
//| из всех графических элементов коллекции                          |
//+------------------------------------------------------------------+
int CGraphElementsCollection::GetMaxID(void)
  {
   int index=CSelect::FindGraphCanvElementMax(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID);
   CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(index);
   return(obj!=NULL ? obj.ID() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+

Здесь: получаем индекс объекта в списке-коллекции, имеющего наибольшее значение идентификатора. Получаем указатель на объект по полученному индексу и возвращаем идентификатор объекта в случае, если указатель на объект был получен. Иначе — возвращаем -1 — либо коллекция графических элементов пуста, либо была ошибка получения указателя.


Тестирование

Для тестирования возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part111\ под новым именем TstDE111.mq5.

Сделаем основную панель побольше в размерах и разместим в её первом объекте-контейнере GroupBox объект CheckBox, четыре объекта RadioButton со значением группы 2, три объекта-кнопки, две из которых будут иметь группу 2, а третья — по умолчанию, унаследованную от группы своего объекта-контейнера — группу 1. Под кнопками разместим ещё два объекта RadioButton со значением группы 3. Таким образом, в этом контейнере у нас будут четыре объекта RadioButton с группой 2, два объекта RadioButton с группой 3 и три кнопки — две с группой 2 и одна с группой 1.

Справа от первого контейнера GroupBox разместим ещё один такой же и создадим внутри него новый объект CheckedListBox, в котором создадим четыре объекта CheckBox. Все объекты, размещённые в разных группах одного контейнера, должны работать как отдельные наборы объектов. Вся визуальная составляющая при взаимодействии объектов с мышкой должна работать удовлетворительно.

В обработчике OnInit() советника разместим такой блок кода для создания всех элементов GUI:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Установка глобальных переменных советника
   ArrayResize(array_clr,2);        // Массив цветов градиентной заливки
   array_clr[0]=C'26,100,128';      // Исходный ≈Тёмно-лазурный цвет
   array_clr[1]=C'35,133,169';      // Осветлённый исходный цвет
//--- Создадим массив с текущим символом и установим его для использования в библиотеке
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания

//--- Создадим объект WinForms Panel
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",50,50,400,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {
      //--- Установим значение Padding равным 4
      pnl.SetPaddingAll(4);
      //--- Установим флаги перемещаемости, автоизменения размеров и режим автоизменения из входных параметров
      pnl.SetMovable(InpMovable);
      pnl.SetAutoSize(InpAutoSize,false);
      pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
      //--- В цикле создадим 2 привязанных объекта-панели
      CPanel *obj=NULL;
      for(int i=0;i<2;i++)
        {
         //--- создадим объект-панель с рассчитанными координатами, шириной 90 и высотой 40
         CPanel *prev=pnl.GetElement(i-1);
         int xb=0, yb=0;
         int x=(prev==NULL ? xb : xb+prev.Width()+20);
         int y=0;
         if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false))
           {
            obj=pnl.GetElement(i);
            if(obj==NULL)
               continue;
            obj.SetBorderSizeAll(3);
            obj.SetBorderStyle(FRAME_STYLE_BEVEL);
            obj.SetBackgroundColor(obj.ChangeColorLightness(obj.BackgroundColor(),4*i),true);
            obj.SetForeColor(clrRed,true);
            //--- Рассчитаем ширину и высоту будущего объекта-текстовой метки
            int w=obj.Width()-obj.BorderSizeLeft()-obj.BorderSizeRight();
            int h=obj.Height()-obj.BorderSizeTop()-obj.BorderSizeBottom();
            //--- Создадим объект-текстовую метку
            obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,0,0,w,h,clrNONE,255,false,false);
            //--- Получаем указатель на вновь созданный объект
            CLabel *lbl=obj.GetElement(0);
            if(lbl!=NULL)
              {
               //--- Если объект имеет чётный, или нулевой индекс в списке - зададим ему цвет текста по умолчанию
               if(i % 2==0)
                  lbl.SetForeColor(CLR_DEF_FORE_COLOR,true);
               //--- Если индекс объекта в списке нечётный - зададим объекту непрозрачность 127
               else
                  lbl.SetForeColorOpacity(127);
               //--- Укажем для шрифта тип толщины "Black" и
               //--- укажем выравнивание текста из настроек советника
               lbl.SetFontBoldType(FW_TYPE_BLACK);
               lbl.SetTextAlign(InpTextAlign);
               lbl.SetAutoSize((bool)InpTextAutoSize,false);
               //--- Для объекта с чётным, или нулевым индексом укажем для текста цену Bid, иначе - цену Ask символа 
               lbl.SetText(GetPrice(i % 2==0 ? SYMBOL_BID : SYMBOL_ASK));
               //--- Укажем толщину, тип и цвет рамки для текстовой метки и обновим модифицированный объект
               lbl.SetBorderSizeAll(1);
               lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle);
               lbl.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               lbl.Update(true);
              }
           }
        }
      //--- Создадим объект WinForms GroupBox1
      CGroupBox *gbox1=NULL;
      //--- Координатой Y GroupBox1 будет являться отступ от прикреплённых панелей на 6 пикселей
      int w=pnl.GetUnderlay().Width();
      int y=obj.BottomEdgeRelative()+6;
      //--- Если прикреплённый объект GroupBox создан
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0,y,200,150,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- получим указатель на объект GroupBox по его индексу в списке прикреплённых объектов с типом GroupBox
         gbox1=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0);
         if(gbox1!=NULL)
           {
            //--- установим тип рамки "вдавленная рамка", цвет рамки как цвет фона основной панели,
            //--- а цвет текста - затемнённый на 1 цвет фона последней прикреплённой панели
            gbox1.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox1.SetBorderColor(pnl.BackgroundColor(),true);
            gbox1.SetForeColor(gbox1.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox1.SetText("GroupBox1");
            //--- Создадим объект CheckBox
            gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,2,10,50,20,clrNONE,255,true,false);
            //--- получим указатель на объект CheckBox по его индексу в списке прикреплённых объектов с типом CheckBox
            CCheckBox *cbox=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,0);
            //--- Если CheckBox создан и указатель на него получен
            if(cbox!=NULL)
              {
               //--- Установим параметры CheckBox из входных параметров советника
               cbox.SetAutoSize((bool)InpCheckAutoSize,false);
               cbox.SetCheckAlign(InpCheckAlign);
               cbox.SetTextAlign(InpCheckTextAlign);
               //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
               cbox.SetText("CheckBox");
               cbox.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
               cbox.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               cbox.SetChecked(true);
               cbox.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)InpCheckState);
              }
            //--- Создадим 4 WinForms-объекта RadioButton
            CRadioButton *rbtn=NULL;
            for(int i=0;i<4;i++)
              {
               //--- Создадим объект RadioButton
               int yrb=(rbtn==NULL ? cbox.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,2,yrb+4,50,20,clrNONE,255,true,false);
               //--- получим указатель на объект RadioButton по его индексу в списке прикреплённых объектов с типом RadioButton
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i);
               //--- Если RadioButton1 создан и указатель на него получен
               if(rbtn!=NULL)
                 {
                  //--- Установим параметры RadioButton из входных параметров советника
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
                  rbtn.SetText("RadioButton"+string(i+1));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(2);
                 }
              }
            //--- Создадим 3 WinForms-объекта Button
            CButton *butt=NULL;
            for(int i=0;i<3;i++)
              {
               //--- Создадим объект Button
               int ybtn=(butt==NULL ? 12 : butt.BottomEdgeRelative()+4);
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,(int)fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+20,ybtn,78,18,clrNONE,255,true,false);
               //--- получим указатель на объект Button по его индексу в списке прикреплённых объектов с типом Button
               butt=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i);
               //--- Если Button создан и указатель на него получен
               if(butt!=NULL)
                 {
                  //--- Установим параметры Button из входных параметров советника
                  butt.SetAutoSize((bool)InpButtonAutoSize,false);
                  butt.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpButtonAutoSizeMode,false);
                  butt.SetTextAlign(InpButtonTextAlign);
                  //--- Зададим цвет текста, стиль и цвет рамки
                  butt.SetForeColor(butt.ChangeColorLightness(CLR_DEF_FORE_COLOR,2),true);
                  butt.SetBorderStyle((ENUM_FRAME_STYLE)InpButtonFrameStyle);
                  butt.SetBorderColor(butt.ChangeColorLightness(butt.BackgroundColor(),-10),true);
                  //--- Установим режим toggle в зависимости от настроек
                  butt.SetToggleFlag(InpButtonToggle);
                  //--- Зададим выводимый текст на кнопке в зависимости от флага toggle
                  string txt="Button"+string(i+1);
                  if(butt.Toggle())
                     butt.SetText("Toggle-"+txt);
                  else
                     butt.SetText(txt);
                  if(i<2)
                    {
                     butt.SetGroup(2);
                     if(butt.Toggle())
                       {
                        butt.SetBackgroundColorMouseOver(butt.ChangeColorLightness(butt.BackgroundColor(),-5));
                        butt.SetBackgroundColorMouseDown(butt.ChangeColorLightness(butt.BackgroundColor(),-10));
                        butt.SetBackgroundColorToggleON(C'0xE2,0xC5,0xB1',true);
                        butt.SetBackgroundColorToggleONMouseOver(butt.ChangeColorLightness(butt.BackgroundColorToggleON(),-5));
                        butt.SetBackgroundColorToggleONMouseDown(butt.ChangeColorLightness(butt.BackgroundColorToggleON(),-10));
                       }
                    }
                 }
              }
            //--- Создадим 2 WinForms-объекта RadioButton
            rbtn=NULL;
            for(int i=0;i<2;i++)
              {
               //--- Создадим объект RadioButton
               int yrb=(rbtn==NULL ? butt.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,butt.CoordXRelative()-4,yrb+3,50,20,clrNONE,255,true,false);
               //--- получим указатель на объект RadioButton по его индексу в списке прикреплённых объектов с типом RadioButton
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i+4);
               //--- Если RadioButton1 создан и указатель на него получен
               if(rbtn!=NULL)
                 {
                  //--- Установим параметры RadioButton из входных параметров советника
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
                  rbtn.SetText("RadioButton"+string(i+5));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(3);
                 }
              }
           }
        }
      //--- Создадим объект WinForms GroupBox2
      CGroupBox *gbox2=NULL;
      //--- Координатой Y GroupBox2 будет являться отступ от прикреплённых панелей на 6 пикселей
      w=gbox1.Width()-1;
      int x=gbox1.RightEdgeRelative()+1;
      int h=gbox1.BottomEdgeRelative()-6;
      //--- Если прикреплённый объект GroupBox создан
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- получим указатель на объект GroupBox по его индексу в списке прикреплённых объектов с типом GroupBox
         gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,1);
         if(gbox2!=NULL)
           {
            //--- установим тип рамки "вдавленная рамка", цвет рамки как цвет фона основной панели,
            //--- а цвет текста - затемнённый на 1 цвет фона последней прикреплённой панели
            gbox2.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox2.SetBorderColor(pnl.BackgroundColor(),true);
            gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox2.SetText("GroupBox2");
            //--- Создадим объект CheckedListBox
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,4,12,80,80,clrNONE,255,true,false);
            //--- получим указатель на объект CheckedListBox по его индексу в списке прикреплённых объектов с типом CheckBox
            CCheckedListBox *clbox=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,0);
            //--- Если CheckedListBox создан и указатель на него получен
            if(clbox!=NULL)
              {
               clbox.CreateCheckBox(4);
              }
           }
        }
      //--- Перерисуем все объекты в порядке их иерархии
      pnl.Redraw(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Здесь логика достаточно подробно расписана в комментариях к коду. Оставим её для самостоятельного изучения. Любые вопросы можно всегда задать в обсуждении статьи.

Скомпилируем советник и запустим его на графике:


Что ж, видим, что заявленный функционал работает верно — одинаковые объекты одного контейнера, но размещённые в разных группах, работают правильно — объекты каждой группы являются самостоятельной единицей и не влияют на работу объектов того же типа из другой группы. Две кнопки-переключателя, объединённые в группу, работают правильно и не затрагивают третью кнопку, которая работает самостоятельно. Объект CheckedListBox в его текущем состоянии тоже правильно работает. Вся визуальная составляющая ведёт себя верно (что не исключает в дальнейшем появления сбоев при смене цвета во время обработки предыдущего состояния мышки и её кнопок). Но все возможные ошибки мы найдём и поправим при последующей разработке графических элементов.


Что дальше

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

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

К содержанию

*Статьи этой серии:

DoEasy. Элементы управления (Часть 1): Первые шаги
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock
DoEasy. Элементы управления (Часть 5): Базовый WinForms-объект, элемент управления "Панель", параметр AutoSize
DoEasy. Элементы управления (Часть 6): Элемент управления "Панель", автоизменение размеров контейнера под внутреннее содержимое
DoEasy. Элементы управления (Часть 7): Элемент управления "Текстовая метка"
DoEasy. Элементы управления (Часть 8): Базовые WinForms-объекты по категориям, элементы управления "GroupBox" и "CheckBox
DoEasy. Элементы управления (Часть 9): Реорганизация методов WinForms-объектов, элементы управления "RadioButton" и "Button"
DoEasy. Элементы управления (Часть 10): WinForms-объекты — оживляем интерфейс



Прикрепленные файлы |
MQL5.zip (4530.53 KB)
Эксперименты с нейросетями (Часть 2): Хитрая оптимизация нейросети Эксперименты с нейросетями (Часть 2): Хитрая оптимизация нейросети
Нейросети наше все. Проверяем на практике, так ли это. MetaTrader 5, как самодостаточное средство для использования нейросетей в трейдинге. Простое объяснение.
Видео: Настройка MetaTrader 5 и MQL5 для простой автоматизированной торговли Видео: Настройка MetaTrader 5 и MQL5 для простой автоматизированной торговли
В этом небольшом видеокурсе вы узнаете, как скачать, установить и настроить MetaTrader 5 для автоматизированной торговли. Вы также узнаете, как настроить график и параметры автоматизированной торговли. Вы проведете свое первое тестирование на истории и узнаете, как импортировать советника, который может самостоятельно торговать 24 часа в сутки 7 дней в неделю, избавляя вас от необходимости сидеть перед экраном.
Разработка торговой системы на основе индикатора ATR Разработка торговой системы на основе индикатора ATR
В этой статье мы изучим новый технический инструмент, который можно использовать в торговле. Это продолжение серии, в которой мы учимся проектировать простые торговые системы. В этот раз мы будем работать с еще одним популярным техническим индикатором — Средний истинный диапазон (Average True Range, ATR).
Разработка торгового советника с нуля (Часть 18): Новая система ордеров (I) Разработка торгового советника с нуля (Часть 18): Новая система ордеров (I)
Это первая часть новой системы ордеров. С тех пор, как мы начали создавать документацию данного советника в наших статьях, он претерпел различные изменения и улучшения, сохраняя при этом ту же модель системы ордеров на графике.