English 中文 Español Deutsch 日本語 Português
preview
DoEasy. Элементы управления (Часть 23): дорабатываем WinForms-объекты TabControl и SplitContainer

DoEasy. Элементы управления (Часть 23): дорабатываем WinForms-объекты TabControl и SplitContainer

MetaTrader 5Примеры | 21 октября 2022, 13:38
714 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

В библиотеке организована событийная модель "общения" графических элементов с курсором мышки. Внутри каждого графического элемента существуют "рабочие" области, отвечающие за то или иное поведение элемента управления при взаимодействии с мышью. У каждого объекта есть, например, активная зона, и если курсор находится внутри этой зоны, то возможно взаимодействие с этим объектом. У объекта есть и область управления, в которой, например, можно располагать кнопки управления формой (свернуть/развернуть/закрыть/и т.п.). Исходя из наличия такой области у объекта, можно организовать дополнительный функционал при взаимодействии курсора с этой областью. Например, для разделителя элемента управления SplitContainer мы можем организовать его работу при помощи обработки события внутри области управления, расположение которой совпадает с расположением разделителя.

Для организации такого функционала добавим новые обработчики событий мышки (впрочем, мы бы их всё равно добавляли для организации обработки курсора внутри области управления) и сделаем работу разделителя элемента управления SplitContainer посредством обработки событий внутри этих обработчиков. Кроме того, исправим выявленные недоработки в элементах управления TabControl и SplitContainer.

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

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


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

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

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

//--- В пределах области разделителя окна
   MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_NOT_PRESSED, // Курсор в пределах области разделителя окна, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_PRESSED,     // Курсор в пределах области разделителя окна, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_WHEEL,       // Курсор в пределах области разделителя окна, прокручивается колёсико мышки
  };
//+------------------------------------------------------------------+

...

//--- В пределах области разделителя окна
   MOUSE_EVENT_INSIDE_SPLITTER_AREA_NOT_PRESSED,      // Курсор в пределах области разделителя окна, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_SPLITTER_AREA_PRESSED,          // Курсор в пределах области разделителя окна, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL,            // Курсор в пределах области разделителя окна, прокручивается колёсико мышки
  };
#define MOUSE_EVENT_NEXT_CODE  (MOUSE_EVENT_INSIDE_SPLITTER_AREA_WHEEL+1)  // Код следующего события после последнего кода события мышки
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Список возможных состояний мышки относительно формы              |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_FORM_STATE
  {
   MOUSE_FORM_STATE_NONE = 0,                         // Неопределённое состояние
//--- За пределами формы
   MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED,         // Курсор за пределами формы, кнопки мышки не нажаты
   MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED,             // Курсор за пределами формы, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL,               // Курсор за пределами формы, прокручивается колёсико мышки
//--- В пределах формы
   MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED,          // Курсор в пределах формы, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_FORM_PRESSED,              // Курсор в пределах формы, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_FORM_WHEEL,                // Курсор в пределах формы, прокручивается колёсико мышки
//--- В пределах области заголовка окна
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED,   // Курсор в пределах активной области, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED,       // Курсор в пределах активной области, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL,         // Курсор в пределах активной области, прокручивается колёсико мышки
   MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED,      // Курсор в пределах активной области, отжата кнопка мышки (левая)
//--- В пределах области прокрутки окна
   MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED,   // Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED,       // Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL,         // Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
//--- В пределах области изменения размеров окна
   MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED,   // Курсор в пределах области изменения размеров окна, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED,       // Курсор в пределах области изменения размеров окна, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL,         // Курсор в пределах области изменения размеров окна, прокручивается колёсико мышки
//--- В пределах области управления
   MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED,  // Курсор в пределах области управления, кнопки мышки не нажаты
   MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED,      // Курсор в пределах области управления, нажата кнопка мышки (любая)
   MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL,        // Курсор в пределах области управления, прокручивается колёсико мышки
  };
//+------------------------------------------------------------------+
//| Список возможных событий мышки                                   |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_EVENT
  {
   MOUSE_EVENT_NO_EVENT = CHART_OBJ_EVENTS_NEXT_CODE, // Нет события
//---
   MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED,              // Курсор за пределами формы, кнопки мышки не нажаты
   MOUSE_EVENT_OUTSIDE_FORM_PRESSED,                  // Курсор за пределами формы, нажата кнопка мышки (любая)
   MOUSE_EVENT_OUTSIDE_FORM_WHEEL,                    // Курсор за пределами формы, прокручивается колёсико мышки
//--- В пределах формы
   MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED,               // Курсор в пределах формы, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_FORM_PRESSED,                   // Курсор в пределах формы, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_FORM_WHEEL,                     // Курсор в пределах формы, прокручивается колёсико мышки
//--- В пределах активной области окна
   MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED,        // Курсор в пределах активной области, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_ACTIVE_AREA_PRESSED,            // Курсор в пределах активной области, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_ACTIVE_AREA_WHEEL,              // Курсор в пределах активной области, прокручивается колёсико мышки
   MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED,           // Курсор в пределах активной области, отжата кнопка мышки (левая)
//--- В пределах области прокрутки окна
   MOUSE_EVENT_INSIDE_SCROLL_AREA_NOT_PRESSED,        // Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_SCROLL_AREA_PRESSED,            // Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL,              // Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
//--- В пределах области изменения размеров окна
   MOUSE_EVENT_INSIDE_RESIZE_AREA_NOT_PRESSED,        // Курсор в пределах области изменения размеров окна, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_RESIZE_AREA_PRESSED,            // Курсор в пределах области изменения размеров окна, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_RESIZE_AREA_WHEEL,              // Курсор в пределах области изменения размеров окна, прокручивается колёсико мышки
//--- В пределах области управления
   MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED,       // Курсор в пределах области управления, кнопки мышки не нажаты
   MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED,           // Курсор в пределах области управления, нажата кнопка мышки (любая)
   MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL,             // Курсор в пределах области управления, прокручивается колёсико мышки
  };
#define MOUSE_EVENT_NEXT_CODE  (MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL+1)   // Код следующего события после последнего кода события мышки
//+------------------------------------------------------------------+

Значение макроподстановки MOUSE_EVENT_NEXT_CODE теперь рассчитывается из значения последней константы перечисления возможных событий мышки.


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

Для установки координат начала области управления и её размеров (ширины и высоты) нам нужно добавить такие методы.

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

//--- (1) Сохраняет графический ресурс в массив, (2) восстанавливает ресурс из массива
   bool              ResourceStamp(const string source);
   virtual bool      Reset(void);
   
//--- Возвращает положение курсора относительно (1) всего элемента, (2) видимой области, (3) активной зоны, (4) зоны управления элемента
   bool              CursorInsideElement(const int x,const int y);
   bool              CursorInsideVisibleArea(const int x,const int y);
   bool              CursorInsideActiveArea(const int x,const int y);
   bool              CursorInsideControlArea(const int x,const int y);

//--- Создаёт элемент

...

//--- Устанавливает флаг (1) перемещаемости, (2) активности объекта, (3) флаг взаимодействия,
//--- (4) идентификатор элемента, (5) номер элемента в списке, (6) флаг доступности, (7) наличие тени
   void              SetMovable(const bool flag)               { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag);                     }
   void              SetActive(const bool flag)                { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag);                      }
   void              SetInteraction(const bool flag)           { this.SetProperty(CANV_ELEMENT_PROP_INTERACTION,flag);                 }
   void              SetID(const int id)                       { this.SetProperty(CANV_ELEMENT_PROP_ID,id);                            }
   void              SetNumber(const int number)               { this.SetProperty(CANV_ELEMENT_PROP_NUM,number);                       }
   void              SetEnabled(const bool flag)               { this.SetProperty(CANV_ELEMENT_PROP_ENABLED,flag);                     }
   void              SetShadow(const bool flag)                { this.m_shadow=flag;                                                   }
   
//--- Устанавливает (1) координату X, (2) координату Y, (3) ширину, (4) высоту зоны управления элемента
   void              SetControlAreaX(const int value)          { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,value);             }
   void              SetControlAreaY(const int value)          { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,value);             }
   void              SetControlAreaWidth(const int value)      { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,value);         }
   void              SetControlAreaHeight(const int value)     { this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,value);        }
   
//--- Возвращает смещение (1) левого, (2) правого, (3) верхнего, (4) нижнего края активной зоны элемента
   int               ActiveAreaLeftShift(void)           const { return (int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT);       }
   int               ActiveAreaRightShift(void)          const { return (int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT);      }
   int               ActiveAreaTopShift(void)            const { return (int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP);        }
   int               ActiveAreaBottomShift(void)         const { return (int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM);     }
//--- Возвращает координату (1) левого, (2) правого, (3) верхнего, (4) нижнего края активной зоны элемента
   int               ActiveAreaLeft(void)                const { return int(this.CoordX()+this.ActiveAreaLeftShift());                 }
   int               ActiveAreaRight(void)               const { return int(this.RightEdge()-this.ActiveAreaRightShift());             }
   int               ActiveAreaTop(void)                 const { return int(this.CoordY()+this.ActiveAreaTopShift());                  }
   int               ActiveAreaBottom(void)              const { return int(this.BottomEdge()-this.ActiveAreaBottomShift());           }

//--- Возвращает смещение координаты (1) X, (2) Y, (3) ширину, (4) высоту, (5) правый, (6) нижний край зоны управления элемента
   int               ControlAreaXShift(void)             const { return (int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X);       }
   int               ControlAreaYShift(void)             const { return (int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y);       }
   int               ControlAreaWidth(void)              const { return (int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH);   }
   int               ControlAreaHeight(void)             const { return (int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT);  }
//--- Возвращает координату (1) левого, (2) правого, (3) верхнего, (4) нижнего края зоны управления элемента
   int               ControlAreaLeft(void)               const { return this.CoordX()+this.ControlAreaXShift();                        }
   int               ControlAreaRight(void)              const { return this.ControlAreaLeft()+this.ControlAreaWidth();                }
   int               ControlAreaTop(void)                const { return this.CoordY()+this.ControlAreaYShift();                        }
   int               ControlAreaBottom(void)             const { return this.ControlAreaTop()+this.ControlAreaHeight();                }
//--- Возвращает относительную координату (1) левого, (2) правого, (3) верхнего, (4) нижнего края зоны управления элемента
   int               ControlAreaLeftRelative(void)       const { return this.ControlAreaLeft()-this.CoordX();                          }
   int               ControlAreaRightRelative(void)      const { return this.ControlAreaRight()-this.CoordX();                         }
   int               ControlAreaTopRelative(void)        const { return this.ControlAreaTop()-this.CoordY();                           }
   int               ControlAreaBottomRelative(void)     const { return this.ControlAreaBottom()-this.CoordY();                        }
   
//--- Возвращает (1) координату X, (2) координату Y, (3) ширину, (4) высоту области прокрутки справа элемента

...

//--- Высота области видимости
   virtual int       VisibleAreaHeight(void)             const { return this.YSize();                                                  }
   virtual bool      SetVisibleAreaHeight(const int value,const bool only_prop)
                       {
                        ::ResetLastError();
                        if((!only_prop && CGBaseObj::SetYSize(value)) || only_prop)
                          {
                           this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,value);
                           return true;
                          }
                        else
                           CMessage::ToLog(DFUN,::GetLastError(),true);
                        return false;
                       }
//--- Устанавливает относительные координаты и размеры видимой области
   void              SetVisibleArea(const int x,const int y,const int w,const int h)
                       {
                        this.SetVisibleAreaX(x,false);
                        this.SetVisibleAreaY(y,false);
                        this.SetVisibleAreaWidth(w,false);
                        this.SetVisibleAreaHeight(h,false);
                       }
//--- Устанавливает размеры видимой области во весь объект
   void              ResetVisibleArea(void)                    { this.SetVisibleArea(0,0,this.Width(),this.Height());                  }
                       
//--- Возвращает (1) координату X, (2) правую границу, (3) координату Y, (4) нижнюю границу видимой области 

Данные методы служат для упрощения установки и получения свойств области видимости графического объекта.


Реализация метода, возвращающего положение курсора относительно видимой области элемента:

//+------------------------------------------------------------------+
//|Возвращает положение курсора относительно видимой области элемента|
//+------------------------------------------------------------------+
bool CGCnvElement::CursorInsideVisibleArea(const int x,const int y)
  {
   return(x>=this.CoordXVisibleArea() && x<=this.RightEdgeVisibleArea() && y>=this.CoordYVisibleArea() && y<=this.BottomEdgeVisibleArea());
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий положение курсора относительно зоны управления элемента:

//+------------------------------------------------------------------+
//|Возвращает положение курсора относительно зоны управления элемента|
//+------------------------------------------------------------------+
bool CGCnvElement::CursorInsideControlArea(const int x,const int y)
  {
   return(x>=this.ControlAreaLeft() && x<=this.ControlAreaRight() && y>=this.ControlAreaTop() && y<=this.ControlAreaBottom());
  }
//+------------------------------------------------------------------+

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

Виртуальные обработчики событий мышки — их объявление, находятся в классе объекта-формы в файле \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, в защищённой секции класса.

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

//--- Обработчик события  Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
   virtual void      MouseScrollAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
   virtual void      MouseScrollAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области управления, кнопки мышки не нажаты
   virtual void      MouseControlAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области управления, нажата кнопка мышки (любая)
   virtual void      MouseControlAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области управления, прокручивается колёсико мышки
   virtual void      MouseControlAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Отправляет сообщение о событии
   virtual bool      SendEvent(const long chart_id,const ushort event_id);

public:


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

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CForm::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)
  {
//--- Создаём новый графический элемент
   CGCnvElement *obj=this.CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
//--- Если объект создан - рисуем добавленный объект и возвращаем true
   if(obj==NULL)
      return false;
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetBase());
   obj.Erase(colour,opacity,redraw);
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает и возвращает состояние мышки относительно формы    |
//+------------------------------------------------------------------+
ENUM_MOUSE_FORM_STATE CForm::MouseFormState(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Расположение данных в ushort-значении состояния кнопок
   //---------------------------------------------------------------------------
   //   bit    |    byte   |            state            |    dec    |   hex   |
   //---------------------------------------------------------------------------
   //    0     |     0     | левая кнопка мыши           |     1     |    1    |
   //---------------------------------------------------------------------------
   //    1     |     0     | правая кнопка мыши          |     2     |    2    |
   //---------------------------------------------------------------------------
   //    2     |     0     | клавиша SHIFT               |     4     |    4    |
   //---------------------------------------------------------------------------
   //    3     |     0     | клавиша CTRL                |     8     |    8    |
   //---------------------------------------------------------------------------
   //    4     |     0     | средняя кнопка мыши         |    16     |   10    |
   //---------------------------------------------------------------------------
   //    5     |     0     | 1 доп. кнопка мыши          |    32     |   20    |
   //---------------------------------------------------------------------------
   //    6     |     0     | 2 доп. кнопка мыши          |    64     |   40    |
   //---------------------------------------------------------------------------
   //    7     |     0     | прокрутка колёсика          |    128    |   80    |
   //---------------------------------------------------------------------------
   //---------------------------------------------------------------------------
   //    0     |     1     | курсор внутри формы         |    256    |   100   |
   //---------------------------------------------------------------------------
   //    1     |     1     | курсор внутри активной зоны |    512    |   200   |
   //---------------------------------------------------------------------------
   //    2     |     1     | курсор в области управления |   1024    |   400   |
   //---------------------------------------------------------------------------
   //    3     |     1     | курсор в области прокрутки  |   2048    |   800   |
   //---------------------------------------------------------------------------
   //    4     |     1     | курсор на левой грани       |   4096    |  1000   |
   //---------------------------------------------------------------------------
   //    5     |     1     | курсор на нижней грани      |   8192    |  2000   |
   //---------------------------------------------------------------------------
   //    6     |     1     | курсор на правой грани      |   16384   |  4000   |
   //---------------------------------------------------------------------------
   //    7     |     1     | курсор на верхней грани     |   32768   |  8000   |
   //---------------------------------------------------------------------------
//--- Получаем состояние мышки относительно формы и состояние кнопок мыши и клавиш Shift и Ctrl
   this.m_mouse_form_state=MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED;
   ENUM_MOUSE_BUTT_KEY_STATE state=this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam);
//--- Получаем флаги состоянии мышки из объекта класса CMouseState и сохраняем их в переменной
   this.m_mouse_state_flags=this.m_mouse.GetMouseFlags();
//--- Если курсор внутри формы
   if(CGCnvElement::CursorInsideElement(this.m_mouse.CoordX(),this.m_mouse.CoordY()))
     {
      //--- Устанавливаем бит 8, отвечающий за флаг "курсор внутри формы"
      this.m_mouse_state_flags |= (0x0001<<8);
      
      //--- Если курсор внутри активной зоны - устанавливаем бит 9 "курсор внутри активной зоны"
      if(CGCnvElement::CursorInsideActiveArea(this.m_mouse.CoordX(),this.m_mouse.CoordY()))
         this.m_mouse_state_flags |= (0x0001<<9);
      //--- иначе - снимаем бит "курсор внутри активной зоны"
      else this.m_mouse_state_flags &=0xFDFF;
      
      //--- Если курсор внутри области управления - устанавливаем бит 10 "курсор внутри области управления"
      if(CGCnvElement::CursorInsideControlArea(this.m_mouse.CoordX(),this.m_mouse.CoordY()))
         this.m_mouse_state_flags |= (0x0001<<10);
      //--- иначе - снимаем бит "курсор внутри области управления"
      else this.m_mouse_state_flags &=0xFBFF;
      
      //--- Если нажата одна из трёх кнопок мыши - проверяем расположение курсора в областях формы и
      //--- возвращаем соответствующее значение нажатой кнопки (в активной зоне, в области управления или в области формы)
      if((this.m_mouse_state_flags & 0x0001)!=0 || (this.m_mouse_state_flags & 0x0002)!=0 || (this.m_mouse_state_flags & 0x0010)!=0)
        {
         //--- Если курсор внутри формы
         if((this.m_mouse_state_flags & 0x0100)!=0)
            this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_FORM_PRESSED;
         //--- Если курсор внутри активной зоны формы
         if((this.m_mouse_state_flags & 0x0200)!=0)
            this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED;
         //--- Если курсор внутри области управления формы
         if((this.m_mouse_state_flags & 0x0400)!=0)
            this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED;
        }
      
      //--- иначе если ни одна кнопка мышки не нажата
      else
        {
         //--- если колесо мышки прокручивается, возвращаем соответствующее значение прокрутки колеса (в активной зоне, в области управления или в области формы)
         //--- Если курсор внутри формы
         if((this.m_mouse_state_flags & 0x0100)!=0)
           {
            //--- Если прокручивается колёсико мышки
            if((this.m_mouse_state_flags & 0x0080)!=0)
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_FORM_WHEEL;
            else
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED;
           }
         //--- Если курсор внутри активной зоны формы
         if((this.m_mouse_state_flags & 0x0200)!=0)
           {
            //--- Если прокручивается колёсико мышки
            if((this.m_mouse_state_flags & 0x0080)!=0)
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL;
            else
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED;
           }
         //--- Если курсор внутри области управления формы
         if((this.m_mouse_state_flags & 0x0400)!=0)
           {
            //--- Если прокручивается колёсико мышки
            if((this.m_mouse_state_flags & 0x0080)!=0)
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL;
            else
               this.m_mouse_form_state=MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED;
           }
        } 
     }
//--- Если курсор снаружи формы
   else
     {
      //--- возвращаем соответствующее значение кнопок в неактивной зоне
      this.m_mouse_form_state=
        (
         ((this.m_mouse_state_flags & 0x0001)!=0 || (this.m_mouse_state_flags & 0x0002)!=0 || (this.m_mouse_state_flags & 0x0010)!=0) ? 
          MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED
        );
     }
   return this.m_mouse_form_state;
  }
//+------------------------------------------------------------------+

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


В обработчик событий мышки добавим обработку новых событий:

//+------------------------------------------------------------------+
//| Обработчик событий мышки                                         |
//+------------------------------------------------------------------+
void CForm::OnMouseEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   switch(id)
     {
      //--- Курсор за пределами формы, кнопки мышки не нажаты
      //--- Курсор за пределами формы, нажата кнопка мышки (любая)
      //--- Курсор за пределами формы, прокручивается колёсико мышки
      case MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED          :
      case MOUSE_EVENT_OUTSIDE_FORM_PRESSED              :
      case MOUSE_EVENT_OUTSIDE_FORM_WHEEL                :
        break;
      //--- Курсор в пределах формы, кнопки мышки не нажаты
      case MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED           :  this.MouseInsideNotPressedHandler(id,lparam,dparam,sparam);       break;
      //--- Курсор в пределах формы, нажата кнопка мышки (любая)
      case MOUSE_EVENT_INSIDE_FORM_PRESSED               :  this.MouseInsidePressedHandler(id,lparam,dparam,sparam);          break;
      //--- Курсор в пределах формы, прокручивается колёсико мышки
      case MOUSE_EVENT_INSIDE_FORM_WHEEL                 :  this.MouseInsideWhellHandler(id,lparam,dparam,sparam);            break;
      //--- Курсор в пределах активной области, кнопки мышки не нажаты
      case MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED    :  this.MouseActiveAreaNotPressedHandler(id,lparam,dparam,sparam);   break;
      //--- Курсор в пределах активной области, нажата кнопка мышки (любая)
      case MOUSE_EVENT_INSIDE_ACTIVE_AREA_PRESSED        :  this.MouseActiveAreaPressedHandler(id,lparam,dparam,sparam);      break;
      //--- Курсор в пределах активной области, прокручивается колёсико мышки
      case MOUSE_EVENT_INSIDE_ACTIVE_AREA_WHEEL          :  this.MouseActiveAreaWhellHandler(id,lparam,dparam,sparam);        break;
      //--- Курсор в пределах активной области, отжата кнопка мышки (левая)
      case MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED       :  this.MouseActiveAreaReleasedHandler(id,lparam,dparam,sparam);     break;
      //--- Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
      case MOUSE_EVENT_INSIDE_SCROLL_AREA_NOT_PRESSED    :  this.MouseScrollAreaNotPressedHandler(id,lparam,dparam,sparam);   break;
      //--- Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
      case MOUSE_EVENT_INSIDE_SCROLL_AREA_PRESSED        :  this.MouseScrollAreaPressedHandler(id,lparam,dparam,sparam);      break;
      //--- Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
      case MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL          :  this.MouseScrollAreaWhellHandler(id,lparam,dparam,sparam);        break;
      //--- Курсор в пределах области управления, кнопки мышки не нажаты
      case MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED   :  this.MouseControlAreaNotPressedHandler(id,lparam,dparam,sparam);  break;
      //--- Курсор в пределах области управления, нажата кнопка мышки (любая)
      case MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED       :  this.MouseControlAreaPressedHandler(id,lparam,dparam,sparam);     break;
      //--- Курсор в пределах области управления, прокручивается колёсико мышки
      case MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL         :  this.MouseControlAreaWhellHandler(id,lparam,dparam,sparam);       break;
      //---MOUSE_EVENT_NO_EVENT
      default: break;
     }
   this.m_mouse_event_last=(ENUM_MOUSE_EVENT)id;
  }
//+------------------------------------------------------------------+

Если идентификатор события (id), переданный в метод — курсор в пределах области управления + нажаты/не нажаты кнопки мышки + состояние колёсика, то вызываются соответствующие виртуальные методы, объявленные нами выше, полноценная реализация которых должна быть сделана в наследуемых классах. А здесь все эти методы ничего не делают, но их реализация тоже должна быть:

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах области управления,     |
//| кнопки мышки не нажаты                                           |
//+------------------------------------------------------------------+
void CForm::MouseControlAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   return;
  }
//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах области управления,     |
//| нажата кнопка мышки (любая)                                      |
//+------------------------------------------------------------------+
void CForm::MouseControlAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   return;
  }
//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах области управления,     |
//| прокручивается колёсико мышки                                    |
//+------------------------------------------------------------------+
void CForm::MouseControlAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   return;
  }
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh класса элемента управления TabControl, в методе, создающем указанное количество вкладок, во всех строках метода, где есть указание главного объекта, добавим или изменим логику:

//+------------------------------------------------------------------+
//| Создаёт указанное количество вкладок                             |
//+------------------------------------------------------------------+
bool CTabControl::CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="")
  {
//--- Рассчитываем размеры и начальные координаты заголовка вкладки
   int w=(tab_w==0 ? this.ItemWidth()  : tab_w);
   int h=(tab_h==0 ? this.ItemHeight() : tab_h);

//--- В цикле по количеству вкладок
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- В зависимости от расположения заголовков вкладок устанавливаем их начальные координаты
      int header_x=2;
      int header_y=2;
      int header_w=w;
      int header_h=h;
      
      //--- Устанавливаем текущую координату X или Y в зависимости от расположения заголовков вкладок
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=2;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=2;
           header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h);
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w-2;
           header_y=(header==NULL ? 2 : header.BottomEdgeRelative());
           break;
         default:
           break;
        }
      //--- Создаём объект TabHeader
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,255,this.Active(),false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      header.SetBase(this.GetObject());
      header.SetPageNumber(i);
      header.SetGroup(this.Group()+1);
      header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
      header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
      header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
      header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
      header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
      header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
      header.SetBorderStyle(FRAME_STYLE_SIMPLE);
      header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
      header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
      header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
      header.SetAlignment(this.Alignment());
      header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      else
         this.SetHeaderText(header,"TabPage"+string(i+1));
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
         header.SetFontAngle(90);
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
         header.SetFontAngle(270);
      header.SetTabSizeMode(this.TabSizeMode());
      
      //--- Сохраняем изначальную высоту заголовка и устанавливаем его размеры в соответствии с режимом установки размеров заголовков
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Получаем смещение по Y расположения заголовка после изменения его высоты и
      //--- сдвигаем его на рассчитанную величину только для расположения заголовков слева
      int y_shift=header.Height()-h_prev;
      if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0)))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      header.SetVisibleFlag(this.IsVisible(),false);
      //--- Записываем в заголовок указатель на предыдущий объект в списке
      CTabHeader *prev=this.GetTabHeader(i-1);
      header.SetPrevHeader(prev);
      
      //--- В зависимости от расположения заголовков вкладок устанавливаем начальные координаты полей вкладок
      int field_x=0;
      int field_y=0;
      int field_w=this.Width();
      int field_h=this.Height()-header.Height()-2;
      int header_shift=0;
      
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           field_x=0;
           field_y=header.BottomEdgeRelative();
           field_w=this.Width();
           field_h=this.Height()-header.Height()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           break;
         default:
           break;
        }
      
      //--- Создаём объект TabField (поле вкладки)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,255,true,false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+1);
      field.SetBorderSizeAll(1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
      field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
      field.SetForeColor(CLR_DEF_FORE_COLOR,true);
      field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
      field.Hide();
     }
//--- Создаём объект кнопки влево-вправо
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
//--- Получаем указатель на вновь созданный объект
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      box_lr.SetBase(this.GetObject());
      box_lr.SetID(this.GetMaxIDAll());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(0);
      box_lr.Hide();
      CArrowLeftButton *lb=box_lr.GetArrowLeftButton();
      if(lb!=NULL)
        {
         lb.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         lb.SetBase(box_lr);
         lb.SetID(this.GetMaxIDAll());
        }
      CArrowRightButton *rb=box_lr.GetArrowRightButton();
      if(rb!=NULL)
        {
         rb.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         rb.SetBase(box_lr);
         rb.SetID(this.GetMaxIDAll());
        }
     }
//--- Создаём объект кнопки вверх-вниз
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false);
//--- Получаем указатель на вновь созданный объект
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
      box_ud.SetBase(this.GetObject());
      box_ud.SetID(this.GetMaxIDAll());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(0);
      box_ud.Hide();
      CArrowDownButton *db=box_ud.GetArrowDownButton();
      if(db!=NULL)
        {
         db.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         db.SetBase(box_ud);
         db.SetID(this.GetMaxIDAll());
        }
      CArrowUpButton *ub=box_ud.GetArrowUpButton();
      if(ub!=NULL)
        {
         ub.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         ub.SetBase(box_ud);
         ub.SetID(this.GetMaxIDAll());
        }
     }
//--- Выстраиваем все заголовки в соответствии с установленными режимами их отображения и выбираем указанную вкладку
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

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

Вместо простого указания главного объекта

SetMain(this.GetMain());

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

SetMain(this.IsMain() ? this.GetObject() : this.GetMain());


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

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

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabHeader.mqh класса-заголовка вкладки доработаем метод, обрезающий изображение, очерченное рассчитываемой прямоугольной областью видимости. Уменьшим на два пикселя размер, на который нужно обрезать заголовки при видимости кнопок со стрелками вверх-вниз. Просто потому, что получающийсяя отступ между заголовком и кнопками слишком велик и выглядит не аккуратно. И скорректируем координату области видимости контейнера для ушедшего за край выбранного заголовка вкладки:

//+------------------------------------------------------------------+
//| Обрезает изображение, очерченное рассчитываемой                  |
//| прямоугольной областью видимости                                 |
//+------------------------------------------------------------------+
void CTabHeader::Crop(void)
  {
//--- Получаем указатель на базовый объект
   CGCnvElement *base=this.GetBase();
//--- Если у объекта нет базового, к которому он прикреплён, то и обрезать скрытые области не нужно - уходим
   if(base==NULL)
      return;
//--- Задаём начальные координаты и размеры области видимости во весь объект
   int vis_x=0;
   int vis_y=0;
   int vis_w=this.Width();
   int vis_h=this.Height();
//--- Задаём размеры областей сверху, снизу, слева и справа, которые выходят за пределы контейнера
   int crop_top=0;
   int crop_bottom=0;
   int crop_left=0;
   int crop_right=0;
//--- Получаем дополнительные размеры, на которые нужно обрезать заголовки при видимости кнопок со стрелками
   int add_size_lr=(this.IsVisibleLeftRightBox() ? this.m_arr_butt_lr_size : 0);
   int add_size_ud=(this.IsVisibleUpDownBox()    ? this.m_arr_butt_ud_size-2 : 0);
   int correct_size_vis=(this.State() ? 0 : 2);
//--- Рассчитываем границы области контейнера, внутри которой объект полностью виден
   int top=fmax(base.CoordY()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP),base.CoordYVisibleArea())+correct_size_vis+(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? add_size_ud : 0);
   int bottom=fmin(base.BottomEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_BOTTOM),base.BottomEdgeVisibleArea()+1)-correct_size_vis-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT ? add_size_ud : 0);
   int left=fmax(base.CoordX()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_LEFT),base.CoordXVisibleArea())+correct_size_vis;
   int right=fmin(base.RightEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_RIGHT),base.RightEdgeVisibleArea()+1)-add_size_lr;

//--- Корректируем координату видимой области в случае, если выбранный заголовок вкладки ушёл за левый или нижний край области
   if(this.State())
     {
      if((this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP || this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM) && this.CoordX()<left)
         left+=4;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT && this.BottomEdge()>bottom)
         bottom-=4;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT && this.CoordY()<top)
         top+=4;
     }

//--- Рассчитываем величины областей сверху, снизу, слева и справа, на которые объект выходит
//--- за пределы границ области контейнера, внутри которой объект полностью виден
   crop_top=this.CoordY()-top;
   if(crop_top<0)
      vis_y=-crop_top;
   crop_bottom=bottom-this.BottomEdge()-1;
   if(crop_bottom<0)
      vis_h=this.Height()+crop_bottom-vis_y;
   crop_left=this.CoordX()-left;
   if(crop_left<0)
      vis_x=-crop_left;
   crop_right=right-this.RightEdge()-1;
   if(crop_right<0)
      vis_w=this.Width()+crop_right-vis_x;
//--- Если есть области, которые необходимо скрыть - вызываем метод обрезания с рассчитанными размерами области видимости объекта
   if(crop_top<0 || crop_bottom<0 || crop_left<0 || crop_right<0)
      this.Crop(vis_x,vis_y,vis_w,vis_h);
  }
//+------------------------------------------------------------------+

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

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

Чтобы избавиться от этого артефакта, нам нужно в классе объекта-поля вкладки в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabField.mqh, в методе, рисующем рамку элемента в зависимости от расположения заголовка, проконтролировать расположение заголовка. И если заголовок выбранный и размещён за краем, то нужно не рисовать линию, сливающую визуально заголовок с полем:

//+------------------------------------------------------------------+
//| Рисует рамку элемента в зависимости от расположения заголовка    |
//+------------------------------------------------------------------+
void CTabField::DrawFrame(void)
  {
//--- Устанавливаем начальные координаты
   int x1=0;
   int y1=0;
   int x2=this.Width()-1;
   int y2=this.Height()-1;
//--- Получаем заголовок вкладки, соответствующий данному полю
   CTabHeader *header=this.GetHeaderObj();
   if(header==NULL)
      return;
//--- Рисуем прямоугольник, полностью очерчивающий поле
   this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity());
//--- В зависимости от расположения заголовка рисуем линию на прилегающей к заголовку грани.
//--- Размер линии рассчитывается от размеров заголовка и соответствует им с отступом в один пиксель с каждой стороны
//--- таким образом визуально грань поля не будет нарисована на прилегающей стороне заголовка
   switch(header.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        if(header.State() && header.CoordX()<this.CoordX())
           return;
        this.DrawLine(header.CoordXRelative()+1,0,header.RightEdgeRelative()-2,0,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        if(header.State() && header.CoordX()<this.CoordX())
           return;
        this.DrawLine(header.CoordXRelative()+1,this.Height()-1,header.RightEdgeRelative()-2,this.Height()-1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        if(header.State() && header.BottomEdge()>this.BottomEdge())
           return;
        this.DrawLine(0,header.BottomEdgeRelative()-2,0,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        if(header.State() && header.CoordY()<this.CoordY())
           return;
        this.DrawLine(this.Width()-1,header.BottomEdgeRelative()-2,this.Width()-1,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

После таких доработок объектов заголовка вкладки и поля вкладки все визуальные артефакты при прокрутке строки заголовков будут убраны.


Поменяем логику взаимодействия мышки с разделителем в классе объекта элемента управления CplitContainer.

При заходе курсора мышки в область управления объекта (в область разделителя), сначала на месте, где должен появиться объект-разделитель, а по-простому — в области управления, нарисуем пунктирный прямоугольник, как это выглядит в элементе управления SplitContainer в MS Visual Studio:

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

Такое поведение не полностью соответствует поведению разделителя в MS Visual Studio, но на вид такое поведение более приятное и не вызывает постоянное появление штрихованного объекта-разделителя, заменяя его ненавязчивым пунктирным прямоугольником, показывающим область взаимодействия.

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

//--- (1) устанавливает, (2) возвращает панель, не изменяющую свои размеры при изменении размеров контейнера
   void              SetFixedPanel(const ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL value)
                       { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_FIXED_PANEL,value);                                                       }
   ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL FixedPanel(void) const
                       { return(ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_FIXED_PANEL);        }
   
   //--- Рисует (1) пустой, (2) пунктирный прямоугольник
   virtual void      DrawRectangleEmpty(void);
   virtual void      DrawRectangleDotted(void);
   
//--- Создаёт новый присоединённый элемент на указанной панели

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

Объявим два обработчика событий — для обработки курсора в области управления и для обработки нажатой кнопки мышки в этой же области:

//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Обработчик события  Курсор в пределах активной области, кнопки мышки не нажаты
   virtual void      MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области управления, кнопки мышки не нажаты
   virtual void      MouseControlAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах области управления, нажата кнопка мышки (любая)
   virtual void      MouseControlAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик последнего события мышки
   virtual void      OnMouseEventPostProcessing(void);


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

//+------------------------------------------------------------------+
//| Создаёт панели                                                   |
//+------------------------------------------------------------------+
void CSplitContainer::CreatePanels(void)
  {
   this.m_list_elements.Clear();
   if(this.SetsPanelParams())
     {
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel1_x,this.m_panel1_y,this.m_panel1_w,this.m_panel1_h,clrNONE,255,true,false))
         return;
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel2_x,this.m_panel2_y,this.m_panel2_w,this.m_panel2_h,clrNONE,255,true,false))
         return;
      for(int i=0;i<2;i++)
        {
         CSplitContainerPanel *panel=this.GetPanel(i);
         if(panel==NULL)
            continue;
         panel.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         panel.SetBase(this.GetObject());
        }
      //---
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLITTER,this.m_splitter_x,this.m_splitter_y,this.m_splitter_w,this.m_splitter_h,clrNONE,255,true,false))
         return;
      CSplitter *splitter=this.GetSplitter();
      if(splitter!=NULL)
        {
         splitter.SetMain(this.IsMain() ? this.GetObject() : this.GetMain());
         splitter.SetBase(this.GetObject());
         splitter.SetMovable(true);
         splitter.SetDisplayed(false);
         splitter.Hide();
        }
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает параметры панелям                                  |
//+------------------------------------------------------------------+
bool CSplitContainer::SetsPanelParams(void)
  {
   switch(this.SplitterOrientation())
     {

      //---...
      //---...

     }
//--- Устанавливаем координаты и размеры области управления, равными свойствам, установленным разделителю
   this.SetControlAreaX(this.m_splitter_x);
   this.SetControlAreaY(this.m_splitter_y);
   this.SetControlAreaWidth(this.m_splitter_w);
   this.SetControlAreaHeight(this.m_splitter_h);
   return true;
  }
//+------------------------------------------------------------------+

Ранее мы их устанавливали при помощи записи свойств посредством методов SetProperty(), что в принципе одно и тоже. Но так, на мой взгляд, понятнее.


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

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CSplitContainer::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Корректируем смещение по Y для подокна
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
//--- Если идентификатор события - перемещение разделителя
   if(id==WF_CONTROL_EVENT_MOVING)
     {
      //--- Получаем указатель на объект-разделитель
      CSplitter *splitter=this.GetSplitter();
      if(splitter==NULL || this.SplitterFixed())
         return;
      //--- Объявляем переменные для координат разделителя
      int x=(int)lparam;
      int y=(int)dparam;
      //--- В зависимости от ориентации разделителя
      switch(this.SplitterOrientation())
        {
         //--- вертикальное положение разделителя
         case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
           //--- Устанавливаем координату Y равной координате Y элемента управления
           y=this.CoordY();
           //--- Корректируем координату X так, чтобы разделитель не выходил за пределы элемента управления
           //--- с учётом получающейся в итоге минимальной ширины панелей
           if(x<this.CoordX()+this.Panel1MinSize())
              x=this.CoordX()+this.Panel1MinSize();
           if(x>this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth())
              x=this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth();
           break;
         //---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL
         //--- горизонтальное положение разделителя
         default:
           //--- Устанавливаем координату X равной координате X элемента управления
           x=this.CoordX();
           //--- Корректируем координату Y так, чтобы разделитель не выходил за пределы элемента управления
           //--- с учётом получающейся в итоге минимальной высоты панелей
           if(y<this.CoordY()+this.Panel1MinSize())
              y=this.CoordY()+this.Panel1MinSize();
           if(y>this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth())
              y=this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth();
           break;
        }
      //--- Рисуем пустой прямоугольник
      this.DrawRectangleEmpty();
      //--- Если разделитель смещён на рассчитанные координаты
      if(splitter.Move(x,y,true))
        {
         //--- устанавливаем разделителю его относительные координаты
         splitter.SetCoordXRelative(splitter.CoordX()-this.CoordX());
         splitter.SetCoordYRelative(splitter.CoordY()-this.CoordY());
         //--- В зависимости от ориентации разделителя устанавливаем его новые координаты
         this.SetSplitterDistance(!this.SplitterOrientation() ? splitter.CoordX()-this.CoordX() : splitter.CoordY()-this.CoordY(),false);
        }
     }
  }
//+------------------------------------------------------------------+

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


Обработчик события  "Курсор в пределах области управления, кнопки мышки не нажаты":

//+------------------------------------------------------------------+
//| Обработчик события  Курсор в пределах области управления,        |
//| кнопки мышки не нажаты                                           |
//+------------------------------------------------------------------+
void CSplitContainer::MouseControlAreaNotPressedHandler(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если разделитель неперемещаемый - уходим
   if(this.SplitterFixed())
      return;
//--- Рисуем пустой прямоугольник в области управления
   this.DrawRectangleEmpty();
//--- Рисуем пунктирный прямоугольник в области управления
   this.DrawRectangleDotted();
//--- Получаем указатель на разделитель
   CSplitter *splitter=this.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- Если разделитель не отображается
   if(!splitter.Displayed())
     {
      //--- Включаем отображение разделителя и показываем его
      splitter.SetDisplayed(true);
      splitter.Erase(true);
      splitter.Show();
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Обработчик события  Курсор в пределах области управления,        |
//| нажата кнопка мышки (любая)                                      |
//+------------------------------------------------------------------+
void CSplitContainer::MouseControlAreaPressedHandler(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если разделитель неперемещаемый - уходим
   if(this.SplitterFixed())
      return;
//--- Рисуем пустой прямоугольник в области управления
   this.DrawRectangleEmpty();
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CSplitContainer::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled() || !this.Displayed())
      return;
   ENUM_MOUSE_FORM_STATE state=this.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              :
      case MOUSE_FORM_STATE_NONE                            :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED  || 
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED         || 
           this.MouseEventLast()==MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED        ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED     ||
           this.MouseEventLast()==MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL       ||
           this.MouseEventLast()==MOUSE_EVENT_NO_EVENT)
          {
            //--- Рисуем пустой прямоугольник на месте области управления
            this.DrawRectangleEmpty();
            //--- Получаем указатель на разделитель
            CSplitter *splitter=this.GetSplitter();
            if(splitter==NULL)
              {
               ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
               return;
              }
            splitter.SetDisplayed(false);
            splitter.Hide();
            this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
          }
        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        :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED  :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED      :
      case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL        :
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED:
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED    :
      case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL      :
        break;
      //---MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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


Метод, рисующий пустой прямоугольник:

//+------------------------------------------------------------------+
//| Рисует пустой прямоугольник                                      |
//+------------------------------------------------------------------+
void CSplitContainer::DrawRectangleEmpty(void)
  {
   int cx1=this.ControlAreaLeftRelative();
   int cx2=this.ControlAreaRightRelative();
   int cy1=this.ControlAreaTopRelative();
   int cy2=this.ControlAreaBottomRelative();
   this.DrawRectangleFill(cx1,cy1,cx2,cy2,CLR_CANV_NULL,0);
   this.Update();
  }
//+------------------------------------------------------------------+

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


Метод, рисующий пунктирный прямоугольник:

//+------------------------------------------------------------------+
//| Рисует пунктирный прямоугольник                                  |
//+------------------------------------------------------------------+
void CSplitContainer::DrawRectangleDotted(void)
  {
   int shift=0;
   int cx1=this.ControlAreaLeftRelative();
   int cx2=fmin(this.ControlAreaRightRelative(),this.VisibleAreaWidth()+2);
   int cy1=this.ControlAreaTopRelative();
   int cy2=this.ControlAreaBottomRelative();
//--- Рисуем точки через одну по верхней границе прямоугольника слева-направо
   for(int x=cx1+1;x<cx2-2;x+=2)
      this.SetPixel(x,cy1,this.ForeColor(),255);
//--- Получаем смещение очередной точки в зависимости от того, где была поставлена последняя точка
   shift=((cx2-cx1-2) %2==0 ? 0 : 1);
//--- Рисуем точки через одну по правой границе прямоугольника сверху-вних
   for(int y=cy1+1+shift;y<cy2-2;y+=2)
      this.SetPixel(cx2-2,y,this.ForeColor(),255);
//--- Получаем смещение очередной точки в зависимости от того, где была поставлена последняя точка
   shift=(this.ControlAreaHeight()-2 %2==0 ? 1 : 0);
//--- Рисуем точки через одну по нижней границе прямоугольника справа-налево
   for(int x=cx2-2-shift;x>cx1;x-=2)
      this.SetPixel(x,cy2-2,this.ForeColor(),255);
//--- Получаем смещение очередной точки в зависимости от того, где была поставлена последняя точка
   shift=((cx2-cx1-2) %2==0 ? 0 : 1);
//--- Рисуем точки через одну по левой границе прямоугольника снизу-вверх
   for(int y=cy2-2-shift;y>cy1;y-=2)
      this.SetPixel(cx1+1,y,this.ForeColor(),255);
//--- Обновляем канвас
   this.Update();
  }
//+------------------------------------------------------------------+

Логика метода расписана в комментариях к коду. Поясню: нам необходимо рисовать линию точками, расположенными через одну. Это делается в четырёх циклах — слева-направо --> сверху-вниз --> справа-налево --> снизу-вверх. Приращение индексов циклов равно двум, поэтому на каждой итерации ставится точка, а координатой служит индекс цикла. Таким образом, при приращении индекса на два, у нас получится ставить точки через одну. Но при этом есть нюанс: если в конце цикла была поставлена точка, то следующий цикл должен начинаться не с точки — чтобы всегда ставить точки через одну. Для этого мы просто рассчитываем ширину и высоту прямоугольника и, в зависимости от чётности/нечётности полученной величины, добавляем к следующей координате либо 1, либо 0. При обратном цикле полученное приращение вычитаем из координаты точки. Таким образом мы получаем пунктирный прямоугольник с точками, всегда рисуемыми через одну. Можно было пойти более простым путём и просто рисовать сглаженный прямоугольник, например, методом DrawPolygonAA(), — он позволяет задать тип рисуемой линии. Но в этом случае, к сожалению, тип линии STYLE_DOT рисует более длинные отрезки, чем в один пиксель, что нам здесь не подходит.


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

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\SplitContainerPanel.mqh в обработчике события "Курсор в пределах активной области, кнопки мышки не нажаты" допишем строку, рисующую пустой прямоугольник в области управления базового объекта (базовым объектом для панели является контейнер элемента управления SplitContainer):

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| кнопки мышки не нажаты                                           |
//+------------------------------------------------------------------+
void CSplitContainerPanel::MouseActiveAreaNotPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Получаем указатель на базовый объект
   CSplitContainer *base=this.GetBase();
//--- Если базовый объект не получен, или разделитель неперемещаемый - уходим
   if(base==NULL || base.SplitterFixed())
      return;
//--- Рисуем пустой прямоугольник на месте области управления базового объекта
   base.DrawRectangleEmpty();
//--- Из базового объекта получаем указатель на объект-разделитель
   CSplitter *splitter=base.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- Если разделитель отображается
   if(splitter.Displayed())
     {
      //--- Выключаем отображение разделителя и скрываем его
      splitter.SetDisplayed(false);
      splitter.Hide();
     }
  }
//+------------------------------------------------------------------+

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


Доработаем вспомогательный объект-разделитель в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\Splitter.mqh.

После недавнего обновления при компиляции библиотеки появилось предупреждение:

deprecated behavior, hidden method calling will be disabled in a future MQL compiler version    SplitContainer.mqh      758     16

Переходя по указанному в журнале адресу попадаем на эту строку в SplitContainer.mqh:

//+------------------------------------------------------------------+
//| Обработчик события  Курсор в пределах области управления,        |
//| кнопки мышки не нажаты                                           |
//+------------------------------------------------------------------+
void CSplitContainer::MouseControlAreaNotPressedHandler(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если разделитель неперемещаемый - уходим
   if(this.SplitterFixed())
      return;
//--- Рисуем пустой прямоугольник в области управления
   this.DrawRectangleEmpty();
//--- Рисуем пунктирный прямоугольник в области управления
   this.DrawRectangleDotted();
//--- Получаем указатель на разделитель
   CSplitter *splitter=this.GetSplitter();
   if(splitter==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER));
      return;
     }
//--- Если разделитель не отображается
   if(!splitter.Displayed())
     {
      //--- Включаем отображение разделителя и показываем его
      splitter.SetDisplayed(true);
      splitter.Erase(true);
      splitter.Show();
     }
  }
//+------------------------------------------------------------------+

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

Точно такой же метод есть в \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh:

//+------------------------------------------------------------------+
//| Полностью очищает элемент                                        |
//+------------------------------------------------------------------+
void CWinFormBase::Erase(const bool redraw=false)
  {
//--- Полностью очищаем элемент с флагом необходимости перерисовки
   CGCnvElement::Erase(redraw);
  }
//+------------------------------------------------------------------+

и в \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh:

//+------------------------------------------------------------------+
//| Полностью очищает элемент                                        |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(const bool redraw=false)
  {
   this.m_canvas.Erase(CLR_CANV_NULL);
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Сигнатура методов идентична, и в итоге всё приводит к методу Erase() объекта-графического элемента CGCnvElement. Поэтому почему компилятор видит неоднозначность — мне не понятно. Но исправим. Добавим метод Erase() в файл \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\Splitter.mqh. А заодно и объявим два обработчика событий мышки:

//--- Перерисовывает объект
   virtual void      Redraw(bool redraw);
//--- Очищает элемент с заполнением его цветом и непрозрачностью
   virtual void      Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Очищает элемент заливкой градиентом
   virtual void      Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false);
//--- Полностью очищает элемент
   virtual void      Erase(const bool redraw=false) { CWinFormBase::Erase(redraw);  }
//--- Обработчик события  Курсор в пределах активной области, нажата кнопка мышки (любая)
   virtual void      MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Обработчик события  Курсор в пределах активной области, отжата кнопка мышки (левая)
   virtual void      MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
  };
//+------------------------------------------------------------------+

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

В методе, рисующем сетку, добавим прозрачности (вместо значения 255 впишем 200), что сделает объект-разделитель немного прозрачным:

//+------------------------------------------------------------------+
//| Рисует сетку                                                     |
//+------------------------------------------------------------------+
void CSplitter::DrawGrid(void)
  {
   for(int y=0;y<this.Height()-1;y++)
      for(int x=0;x<this.Width();x++)
         this.SetPixel(x,y,this.ForeColor(),uchar(y%2==0 ? (x%2==0 ? 200 : 0) : (x%2==0 ? 0 : 200)));
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| нажата кнопка мышки (любая)                                      |
//+------------------------------------------------------------------+
void CSplitter::MouseActiveAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если разделитель не отображается
   if(!this.Displayed())
     {
      //--- Включаем отображение разделителя и показываем его
      this.SetDisplayed(true);
      this.Show();
     }
//--- Перерисовываем разделитель
   this.Redraw(true);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CSplitter::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   this.SetDisplayed(false);
   this.Hide();
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

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


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

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

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

//+------------------------------------------------------------------+
//| Ищет объекты взаимодействия                                      |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::SearchInteractObj(CForm *form,const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если передан не пустой указатель
   if(form!=NULL)
     {
      //--- Создадим список объектов взаимодействия
      int total=form.CreateListInteractObj();
      //--- В цикле по созданному списку
      for(int i=total-1;i>WRONG_VALUE;i--)
        {
         //--- получаем очередной объект-форму
         CForm *obj=form.GetInteractForm(i);
         //--- Если объёкт получен, но он не видимый, или не активен, или не должен отображаться - пропускаем его
         if(obj==NULL || !obj.IsVisible() || !obj.Enabled() || !obj.Displayed())
            continue;
         
         //--- Если объект-форма - элемент управления TabControl, то возвращаем выбранную вкладку, находящуюся под курсором
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
           {
            CTabControl *tab_ctrl=obj;
            CForm *elm=tab_ctrl.SelectedTabPage();
            if(elm!=NULL && elm.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
               return elm;
           }
         
         //--- Если объект-форма - элемент управления SplitContainer или панель элемента управления SplitContainer,
         //--- и если курсор находится на выступающей за края панели области, то такой объект пропускаем
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER || obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL)
           {
            if(!obj.CursorInsideVisibleArea(this.m_mouse.CoordX(),this.m_mouse.CoordY()))
               continue;
           }
         
         //--- Если объект-форма прикреплён к панели элемента управления SplitContainer,
         //--- и если объект выходит за края панели, а курсор находится на выступающей за края панели области, то такой объект пропускаем
         CForm *base=obj.GetBase();
         if(base!=NULL && base.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL)
           {
            if(!obj.CursorInsideVisibleArea(this.m_mouse.CoordX(),this.m_mouse.CoordY()))
               continue;
           }
         
         //--- Если курсор мышки находится над объектом - возвращаем указатель на этот объект
         if(obj.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return obj;
        }
     }
//--- Возвращаем обратно тот же указатель
   return form;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает указатель на форму, находящуюся под курсором          |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::GetFormUnderCursor(const int id, 
                                                    const long &lparam, 
                                                    const double &dparam, 
                                                    const string &sparam,
                                                    ENUM_MOUSE_FORM_STATE &mouse_state,
                                                    long &obj_ext_id,
                                                    int &form_index)
  {
//--- Устанавливаем идентификатор расширенного стандартного графического объекта в -1 
//--- и индекс точки привязки, которой управляет форма, в -1
   obj_ext_id=WRONG_VALUE;
   form_index=WRONG_VALUE;
//--- Инициализируем состояние мышки относительно формы
   mouse_state=MOUSE_FORM_STATE_NONE;
//--- Объявим указатели на объекты класса-коллекции графических элементов
   CGCnvElement *elm=NULL;
   CForm *form=NULL;
//--- Получим список объектов, для которых установлен флаг взаимодействия (должен быть всего один объект)
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(GetListCanvElm(),CANV_ELEMENT_PROP_INTERACTION,true,EQUAL);
//--- Если список получить удалось и он не пустой
   if(list!=NULL && list.Total()>0)
     {
      //--- Получаем единственный в нём графический элемент
      elm=list.At(0);
      //--- Если этот элемент - объект-форма, или её наследники
      if(elm.TypeGraphElement()>=GRAPH_ELEMENT_TYPE_WF_BASE && elm.IsVisible())
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
           {
            //--- Найдём объект взаимодействия.
            //--- Это будет либо найденный объект, либо та же форма
            form=this.SearchInteractObj(form,id,lparam,dparam,sparam);
            //--- Получим состояние мышки найденного объекта
            mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
            //--- Возвращаем объект-форму
            //Comment(form.TypeElementDescription()," ",form.Name(),", ZOrder: ",form.Zorder(),", Interaction: ",form.Interaction());
            return form;
           }
        }
     }
//--- Если нет ни одного объекта-формы с установленным флагом взаимодействия -
//--- в цикле по всем объектам класса-коллекции графических элементов
   int total=this.m_list_all_canv_elm_obj.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной элемент
      elm=this.m_list_all_canv_elm_obj.At(i);
      if(elm==NULL || !elm.IsVisible() || !elm.Enabled() || !elm.Displayed())
         continue;
      //--- если полученный элемент - объект-форма, или её наследники
      if(elm.TypeGraphElement()>=GRAPH_ELEMENT_TYPE_WF_BASE)
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы - возвращаем указатель на форму
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
           {
            //--- Найдём объект взаимодействия.
            //--- Это будет либо найденный объект, либо та же форма
            form=this.SearchInteractObj(form,id,lparam,dparam,sparam);
            //--- Получим состояние мышки найденного объекта
            mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
            //--- Возвращаем объект-форму
            //Comment(form.TypeElementDescription()," ",form.Name(),", ZOrder: ",form.Zorder(),", Interaction: ",form.Interaction());
            return form;
           }
        }
     }
//--- Если нет ни одного объекта-формы из списка-коллекции -
//--- Получаем список расширенных стандартных графических объектов
   list=this.GetListStdGraphObjectExt();
   if(list!=NULL)
     {
      //--- в цикле по всем расширенным стандартным графическим объектам
      for(int i=0;i<list.Total();i++)
        {
         //--- получаем очередной графический объект,
         CGStdGraphObj *obj_ext=list.At(i);
         if(obj_ext==NULL)
            continue;
         //--- получаем объект его инструментария,
         CGStdGraphObjExtToolkit *toolkit=obj_ext.GetExtToolkit();
         if(toolkit==NULL)
            continue;
         //--- обрабатываем событие изменения графика для текущего графического объекта
         obj_ext.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
         //--- Получаем общее количество объектов-форм, созданных для текущего графического объекта
         total=toolkit.GetNumControlPointForms();
         //--- В цикле по всем объектам-формам
         for(int j=0;j<total;j++)
           {
            //--- получаем очередной объект-форму,
            form=toolkit.GetControlPointForm(j);
            if(form==NULL)
               continue;
            //--- получаем состояние мышки относительно формы
            mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
            //--- Если курсор в пределах формы,
            if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
              {
               //--- записываем идентификатор объекта и индекс формы
               //--- и возвращаем указатель на форму
               obj_ext_id=obj_ext.ObjectID();
               form_index=j;
               return form;
              }
           }
        }
     }
//--- Ничего не нашли - возвращаем NULL
   return NULL;
  }
//+------------------------------------------------------------------+


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

            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах формы, нажата кнопка мышки (любая)                     |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED)
              {
               this.SetChartTools(::ChartID(),false);
               //--- Если ещё не установлен флаг удержания формы
               if(!pressed_form)
                 {
                  pressed_form=true;      // ставим флаг нажатия на форме
                  pressed_chart=false;    // снимаем флаг нажатия на графике
                 }
               CForm *main=form.GetMain();
               if(main!=NULL)
                  main.BringToTop();
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_FORM_PRESSED,lparam,dparam,sparam);
               ::ChartRedraw(form.ChartID());
              }
            //+---------------------------------------------------------------------------------------------+

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


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

            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах активной области, нажата кнопка мышки (любая)          |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
              {
               pressed_form=true;                                       // флаг удержания кнопки мышки на форме
               //--- Если зажата левая кнопка мышки
               if(this.m_mouse.IsPressedButtonLeft())
                 {
                  //--- Установим флаги и параметры формы
                  move=true;                                            // флаг перемещения
                  form.SetInteraction(true);                            // флаг взаимодействия формы со внешней средой
                  CForm *main=form.GetMain();
                  if(main!=NULL)
                     main.BringToTop();
                  form.BringToTop();                                    // форма на передний план - поверх всех остальных
                  form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Смещение курсора относительно координаты X
                  form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Смещение курсора относительно координаты Y
                  this.ResetAllInteractionExeptOne(form);               // Сбросим флаги взаимодействия для всех форм, кроме текущей
                  
                  //--- Получаем максимальный ZOrder
                  long zmax=this.GetZOrderMax();
                  //--- Если максимальный ZOrder получен, при этом ZOrder формы меньше максимального, или максимальный ZOrder всех форм равен нулю
                  if(zmax>WRONG_VALUE && (form.Zorder()<zmax || zmax==0))
                    {
                     //--- Если форма - не контрольная точка управления расширенным стандартным графическим объектом -
                     //--- ставим ZOrder формы выше всех
                     if(form.Type()!=OBJECT_DE_TYPE_GFORM_CONTROL)
                        this.SetZOrderMAX(form);
                    }
                 }
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_ACTIVE_AREA_PRESSED,lparam,dparam,sparam);
               ::ChartRedraw(form.ChartID());
              }
            //+---------------------------------------------------------------------------------------------+


И добавим три новых блока для обработки новых событий курсора мышки внутри области управления объекта-формы:

            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах области прокрутки окна, прокручивается колёсико мышки  |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL)
              {
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL,lparam,dparam,sparam);
              }
            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах области управления, кнопки мышки не нажаты             |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED)
              {
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_CONTROL_AREA_NOT_PRESSED,lparam,dparam,sparam);
              }
            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах области управления, нажата кнопка мышки (любая)        |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED)
              {
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_CONTROL_AREA_PRESSED,lparam,dparam,sparam);
              }
            //+---------------------------------------------------------------------------------------------+
            //| Обработчик события Курсор в пределах области управления, прокручивается колёсико мышки      |
            //+---------------------------------------------------------------------------------------------+
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL)
              {
               form.OnMouseEvent(MOUSE_EVENT_INSIDE_CONTROL_AREA_WHEEL,lparam,dparam,sparam);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Именно отсюда вызываются обработчики событий объектов при нахождении курсора внутри области управления.

У нас всё готово для тестирования. Проверим что получилось.


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

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

Доработки минимальные: для указания количества создаваемых панелей у нас есть макроподстановка. Впишем туда значение 1 — создавать будем пока только одну панель:

//+------------------------------------------------------------------+
//|                                                     TstDE123.mq5 |
//|                                  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"
//--- includes
#include <DoEasy\Engine.mqh>
//--- defines
#define  FORMS_TOTAL (1)   // Количество создаваемых форм
#define  START_X     (4)   // Начальная координата X фигуры
#define  START_Y     (4)   // Начальная координата Y фигуры
#define  KEY_LEFT    (65)  // (A) Влево
#define  KEY_RIGHT   (68)  // (D) Вправо
#define  KEY_UP      (87)  // (W) Вверх
#define  KEY_DOWN    (88)  // (X) Вниз
#define  KEY_FILL    (83)  // (S) Заполнение
#define  KEY_ORIGIN  (90)  // (Z) По умолчанию
#define  KEY_INDEX   (81)  // (Q) По индексу


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

//+------------------------------------------------------------------+
//| 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;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      pnl=engine.CreateWFPanel("WinForms Panel"+(string)i,(i==0 ? 50 : 70),(i==0 ? 50 : 70),410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         Print(DFUN,"Описание панели: ",pnl.Description(),", Тип и имя: ",pnl.TypeElementDescription()," ",pnl.Name());
         //--- Установим значение Padding равным 4
         pnl.SetPaddingAll(3);
         //--- Установим флаги перемещаемости, автоизменения размеров и режим автоизменения из входных параметров
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
   
         //--- Создадим элемент управления TabControl
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-30,pnl.Height()-40,clrNONE,255,true,false);
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
         if(tc!=NULL)
           {
            tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
            tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
            tc.SetMultiline(InpTabCtrlMultiline);
            tc.SetHeaderPadding(6,0);
            tc.CreateTabPages(15,0,56,20,TextByLanguage("Вкладка","TabPage"));
            //--- Создадим на каждой вкладке текстовую метку с описанием вкладки
            for(int j=0;j<tc.TabPages();j++)
              {
               tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,322,120,80,20,clrDodgerBlue,255,true,false);
               CLabel *label=tc.GetTabElement(j,0);
               if(label==NULL)
                  continue;
               //--- Если это самая первая вкладка, то текста не будет
               label.SetText(j<5 ? "" : "TabPage"+string(j+1));
              }
            for(int n=0;n<5;n++)
              {
               //--- Создадим на каждой вкладке элемент управления SplitContainer
               tc.CreateNewElement(n,GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,10,10,tc.Width()-22,tc.GetTabField(0).Height()-22,clrNONE,255,true,false);
               //--- Получим элемент управления SplitContainer с каждой вкладки
               CSplitContainer *split_container=tc.GetTabElementByType(n,GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,0);
               if(split_container!=NULL)
                 {
                  //--- Для каждой чётной вкладки разделитель будет вертикальным, для нечётной - горизонтальным
                  split_container.SetSplitterOrientation(n%2==0 ? CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL : CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL,true);
                  //--- Дистанция разделителя на каждой вкладке будет 50 пикселей
                  split_container.SetSplitterDistance(50,true);
                  //--- Ширина разделителя на каждой последующей вкладке будет увеличиваться на 2 пикселя
                  split_container.SetSplitterWidth(6+2*n,false);
                  //--- Для вкладки с индексом 2 сделаем фиксированный разделитель, на остальных - перемещаемый
                  split_container.SetSplitterFixed(n==2 ? true : false);
                  //--- Для вкладки с индексом 3 вторая панель будет в свёрнутом состоянии (видна только первая)
                  if(n==3)
                     split_container.SetPanel2Collapsed(true);
                  //--- Для вкладки с индексом 4 первая панель будет в свёрнутом состоянии (видна только вторая)
                  if(n==4)
                     split_container.SetPanel1Collapsed(true);
                  //--- На каждой из панелей элемента ...
                  for(int j=0;j<2;j++)
                    {
                     CSplitContainerPanel *panel=split_container.GetPanel(j);
                     if(panel==NULL)
                        continue;
                     //--- ... создадим текстовую метку с названием панели
                     if(split_container.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,4,4,panel.Width()-8,panel.Height()-8,clrDodgerBlue,255,true,false))
                       {
                        CLabel *label=split_container.GetPanelElementByType(j,GRAPH_ELEMENT_TYPE_WF_LABEL,0);
                        if(label==NULL)
                           continue;
                        label.SetTextAlign(ANCHOR_CENTER);
                        label.SetText(TextByLanguage("Панель","Panel")+string(j+1));
                       }
                    }
                 }
              }
           }
        }
     }
//--- Отобразим и перерисуем все созданные панели
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      pnl=engine.GetWFPanel("WinForms Panel"+(string)i);
      if(pnl!=NULL)
        {
         pnl.Show();
         pnl.Redraw(true);
        }
     }
        
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

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


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

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


Что дальше

В следующей статье продолжим развитие элементов управления библиотеки.

Ниже прикреплены все файлы текущей версии библиотеки, файлы тестового советника и индикатора контроля событий графиков для MQL5.

К содержанию

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

 
DoEasy. Элементы управления (Часть 20): WinForms-объект SplitContainer
DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей
DoEasy. Элементы управления (Часть 22): SplitContainer. Изменение свойств созданного объекта



Прикрепленные файлы |
MQL5.zip (4570.26 KB)
Нейросети — это просто (Часть 31): Эволюционные алгоритмы Нейросети — это просто (Часть 31): Эволюционные алгоритмы
В предыдущей статье мы начали изучение безградиентных методов оптимизации. И познакомились с генетическим алгоритмом. Сегодня мы продолжаем начатую тему. И рассмотрим ещё один класс эволюционных алгоритмов.
Машинное обучение и Data Science — Нейросети (Часть 01): Разбираем нейронные сети с прямой связью Машинное обучение и Data Science — Нейросети (Часть 01): Разбираем нейронные сети с прямой связью
Многие любят, но немногие понимают все операции, лежащие в основе нейронных сетей. В этой статье я постараюсь простым языком объяснить все, что происходит за закрытыми дверями многоуровневого перцептрона с прямой связью Feed Forward.
Разработка торговой системы на основе индекса силы медведей Bears Power Разработка торговой системы на основе индекса силы медведей Bears Power
Представляю вашему вниманию новую статью из серии, в которой мы учимся строить торговые системы на основе самых популярных индикаторов. На этот раз мы поговорим об Индексе силы медведей Bears Power и создадим торговую систему по его показателям.
Разработка торгового советника с нуля (Часть 29): Говорящая платформа Разработка торгового советника с нуля (Часть 29): Говорящая платформа
В этой статье мы научимся, как заставить платформу MT5 говорить. А что если мы сделаем советника более веселым? Торговля на финансовых рынках часто является чрезвычайно скучным и монотонным занятием, но мы можем сделать эту работу менее утомительной. Этот проект может стать опасным, если у вас есть проблема, делающая вас зависимым, но на самом деле весь сценарий с модификациями может быть более увлекательным и менее скучным.