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

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

MetaTrader 5Примеры | 23 сентября 2022, 19:41
846 3
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

Таким образом мы будем иметь входные данные по главному объекту, по базовому, внутри которого было событие в каком-то из прикреплённых к нему элементов, и имя этого элемента, в котором и было событие. Чтобы точно знать какой же это был элемент управления, в котором мы щёлкнули по кнопке (частный случай), мы в параметре dparam будем отправлять тип этого базового объекта. Таким образом, зная тип базового объекта, мы в главном объекте получим список всех элементов управления с типом, записанным в dparam, и затем в цикле по всем этим объектам поищем прикреплённый к нему объект с именем, передаваемым последним в sparam, в котором было событие (щелчок по элементу управления).

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

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

Но, хватит теории, займёмся делом...


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

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

В файле \MQL5\Include\DoEasy\Defines.mqh в списке возможных событий элементов управления WinForms добавим новые константы перечисления:

//+------------------------------------------------------------------+
//| Список возможных событий элементов управления WinForms           |
//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
   WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,// Нет события
   WF_CONTROL_EVENT_CLICK,                            // Событие "Щелчок по элементу управления"
   WF_CONTROL_EVENT_CLICK_CANCEL,                     // Событие "Отмена щелчка по элементу управления"
   WF_CONTROL_EVENT_TAB_SELECT,                       // Событие "Выбор вкладки элемента управления TabControl"
   WF_CONTROL_EVENT_CLICK_SCROLL_LEFT,                // Событие "Щелчок по кнопке влево элемента управления"
   WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT,               // Событие "Щелчок по кнопке влево элемента управления"
   WF_CONTROL_EVENT_CLICK_SCROLL_UP,                  // Событие "Щелчок по кнопке влево элемента управления"
   WF_CONTROL_EVENT_CLICK_SCROLL_DOWN,                // Событие "Щелчок по кнопке влево элемента управления"
  };
#define WF_CONTROL_EVENTS_NEXT_CODE (WF_CONTROL_EVENT_TAB_SELECT+1)  // Код следующего события после последнего кода события графических элементов
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Целочисленные свойства графического элемента на канвасе          |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Идентификатор элемента
   CANV_ELEMENT_PROP_TYPE,                            // Тип графического элемента

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

   CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,              // Ширина области видимости
   CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,             // Высота области видимости
   CANV_ELEMENT_PROP_DISPLAYED,                       // Флаг отображения не скрытого элемента управления
   CANV_ELEMENT_PROP_GROUP,                           // Группа, к которой принадлежит графический элемент
   CANV_ELEMENT_PROP_ZORDER,                          // Приоритет графического объекта на получение события нажатия мышки на графике

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

   CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,                 // Номер столбца вкладки
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Местоположение объекта внутри элемента управления
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (97)          // Общее количество целочисленных свойств
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Количество неиспользуемых в сортировке целочисленных свойств
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Возможные критерии сортировки графических элементов на канвасе   |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Сортировка по целочисленным свойствам
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Сортировать по идентификатору элемента
   SORT_BY_CANV_ELEMENT_TYPE,                         // Сортировать по типу графического элемента

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

   SORT_BY_CANV_ELEMENT_VISIBLE_AREA_WIDTH,           // Сортировать по ширине области видимости
   SORT_BY_CANV_ELEMENT_VISIBLE_AREA_HEIGHT,          // Сортировать по высоте области видимости
   SORT_BY_CANV_ELEMENT_DISPLAYED,                    // Сортировать по флагу отображения не скрытого элемента управления
   SORT_BY_CANV_ELEMENT_GROUP,                        // Сортировать по группе, к которой принадлежит графический элемент
   SORT_BY_CANV_ELEMENT_ZORDER,                       // Сортировать по приоритету графического объекта на получение события нажатия мышки на графике

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

   SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN,              // Сортировать по номеру столбца вкладки
   SORT_BY_CANV_ELEMENT_ALIGNMENT,                    // Сортировать по местоположению объекта внутри элемента управления
//--- Сортировка по вещественным свойствам

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

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


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

   MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,                 // Запрос за пределами массива
   MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY,    // Не удалось преобразовать координаты графического объекта в экранные
   MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY,    // Не удалось преобразовать координаты время/цена в экранные
   MSG_LIB_SYS_FAILED_ENQUEUE_EVENT,                  // Не удалось поставить событие в очередь событий графика

...

   MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,          // Ширина области видимости
   MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,         // Высота области видимости
   MSG_CANV_ELEMENT_PROP_DISPLAYED,                   // Флаг отображения не скрытого элемента управления
   MSG_CANV_ELEMENT_PROP_ENABLED,                     // Флаг доступности элемента
   MSG_CANV_ELEMENT_PROP_FORE_COLOR,                  // Цвет текста по умолчанию для всех объектов элемента управления

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

   {"Запрос за пределами массива","Data requested outside the array"},
   {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"},
   {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"},
   {"Не удалось поставить событие в очередь событий графика","Failed to put event in chart event queue"},

...

   {"Ширина области видимости","Width of object visibility area"},
   {"Высота области видимости","Height of object visibility area"},
   {"Флаг отображения не скрытого элемента управления","Flag that sets the display of a non-hidden control"},
   {"Флаг доступности элемента","Element Availability Flag"},
   {"Цвет текста по умолчанию для всех объектов элемента управления","Default text color for all objects in the control"},


Доработаем класс объекта-графического элемента в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

В структуру объекта добавим новое вновь добавленное свойство:

private:
   int               m_shift_coord_x;                          // Смещение координаты X относительно базового объекта
   int               m_shift_coord_y;                          // Смещение координаты Y относительно базового объекта
   struct SData
     {         
      //--- Целочисленные свойства объекта
      int            id;                                       // Идентификатор элемента
      int            type;                                     // Тип графического элемента
               
      //---... 
      //---... 
               
      int            visible_area_w;                           // Ширина области видимости
      int            visible_area_h;                           // Высота области видимости
      bool           displayed;                                // Флаг отображения не скрытого элемента управления
      //--- Вещественные свойства объекта
               
      //--- Строковые свойства объекта
      uchar          name_obj[64];                             // Имя объекта-графического элемента
      uchar          name_res[64];                             // Имя графического ресурса
      uchar          text[256];                                // Текст графического элемента
      uchar          descript[256];                            // Описание графического элемента
     };        
   SData             m_struct_obj;                             // Структура объекта
   uchar             m_uchar_array[];                          // uchar-массив структуры объекта

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

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

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+
//--- Устанавливает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) правый, (6) нижний край элемента,
   virtual bool      SetCoordX(const int coord_x);
   virtual bool      SetCoordY(const int coord_y);
   virtual bool      SetWidth(const int width);
   virtual bool      SetHeight(const int height);
   void              SetRightEdge(void)                        { this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());           }
   void              SetBottomEdge(void)                       { this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());         }
//--- Устанавливает смещение (1) левого, (2) верхнего, (3) правого, (4) нижнего края активной зоны относительно элемента,
//--- (5) все смещения краёв активной зоны относительно элемента, (6) непрозрачность
   void              SetActiveAreaLeftShift(const int value)   { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value));       }
   void              SetActiveAreaRightShift(const int value)  { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value));      }
   void              SetActiveAreaTopShift(const int value)    { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value));        }
   void              SetActiveAreaBottomShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value));     }
   void              SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift);
   void              SetOpacity(const uchar value,const bool redraw=false);
   
//--- (1) Устанавливает, (2) возвращает флаг отображения не скрытого элемента управления
   void              SetDisplayed(const bool flag)             { this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,flag);                   }
   bool              Displayed(void)                           { return (bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED);           }

//--- (1) Устанавливает, (2) возвращает тип графического элемента
   void              SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type)
                       {
                        CGBaseObj::SetTypeElement(type);
                        this.SetProperty(CANV_ELEMENT_PROP_TYPE,type);
                       }
   ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(void)  const { return (ENUM_GRAPH_ELEMENT_TYPE)this.GetProperty(CANV_ELEMENT_PROP_TYPE);   }

Методы просто записывают в свойство объекта переданный флаг и возвращают записанное в свойстве объекта значение.


В обоих конструкторах класса впишем в новое свойство значение по умолчанию:

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   descript,
                           const int      x,
                           const int      y,
                           const int      w,
                           const int      h,
                           const color    colour,
                           const uchar    opacity,
                           const bool     movable=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.SetBackgroundColor(colour,true);
   this.SetOpacity(opacity);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Имя графического ресурса
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Идентификатор графика

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

      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                  // Ширина области видимости
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                 // Высота области видимости
      this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                        // Флаг отображения не скрытого элемента управления
      //---
      this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM);  // Принадлежность графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0);                              // Приоритет графического объекта на получение события нажатия мышки на графике
      this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL);                   // Тип толщины шрифта

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

      this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                                                    // Текст графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                                       // Описание графического элемента
      this.SetVisibleFlag(false,false);
     }
   else
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
     }
  }
//+------------------------------------------------------------------+
//| Защищённый конструктор                                           |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const long    chart_id,
                           const int     wnd_num,
                           const string  descript,
                           const int     x,
                           const int     y,
                           const int     w,
                           const int     h) : m_shadow(false)
  {
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.m_type_element=element_type;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.SetBackgroundColor(CLR_CANV_NULL,true);
   this.SetOpacity(0);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,false))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Имя графического ресурса
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Идентификатор графика

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

      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                  // Ширина области видимости
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                 // Высота области видимости
      this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                        // Флаг отображения не скрытого элемента управления
      //---
      this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM);  // Принадлежность графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0);                              // Приоритет графического объекта на получение события нажатия мышки на графике
      this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL);                   // Тип толщины шрифта

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

      this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                                                    // Текст графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                                       // Описание графического элемента
      this.SetVisibleFlag(false,false);
     }
   else
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
     }
  }
//+------------------------------------------------------------------+

По умолчанию для объекта установлена его видимость и отображение при включении видимости базового или главного объекта. Чтобы установить для объекта режим ручного управления его видимостью, значение флага нужно установить в false. В этом случае, если базовый или главный объект был скрыт, а потом отображён, то этот текущий объект всё равно останется в скрытом состоянии, а для его отображения нужно будет самостоятельно установить свойство Displayed в положение true и вызвать метод Show() этого объекта.


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

//+------------------------------------------------------------------+
//| Создаёт структуру объекта                                        |
//+------------------------------------------------------------------+
bool CGCnvElement::ObjectToStruct(void)
  {
//--- Сохранение целочисленных свойств
   this.m_struct_obj.id=(int)this.GetProperty(CANV_ELEMENT_PROP_ID);                               // Идентификатор элемента
   this.m_struct_obj.type=(int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                           // Тип графического элемента
   //---...
   //---...
   this.m_struct_obj.belong=(int)this.GetProperty(CANV_ELEMENT_PROP_BELONG);                       // Принадлежность графического элемента
   this.m_struct_obj.number=(int)this.GetProperty(CANV_ELEMENT_PROP_NUM);                          // Номер элемента в списке
   //---...
   //---...
   this.m_struct_obj.visible_area_x=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_X);       // Координата X области видимости
   this.m_struct_obj.visible_area_y=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_Y);       // Координата Y области видимости
   //---...
   //---...
   this.m_struct_obj.visible_area_w=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH);   // Ширина области видимости
   this.m_struct_obj.visible_area_h=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT);  // Высота области видимости
   this.m_struct_obj.displayed=(bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED);                // Флаг отображения не скрытого элемента управления
   this.m_struct_obj.zorder=this.GetProperty(CANV_ELEMENT_PROP_ZORDER);                            // Приоритет графического объекта на получение события нажатия мышки на графике
   this.m_struct_obj.enabled=(bool)this.GetProperty(CANV_ELEMENT_PROP_ENABLED);                    // Флаг доступности элемента
   this.m_struct_obj.fore_color=(color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR);             // Цвет текста по умолчанию для всех объектов элемента управления
   //---...
   //---...
   this.m_struct_obj.fore_color_opacity=(uchar)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY); // Непрозрачность цвета текста по умолчанию для всех объектов элемента управления
   this.m_struct_obj.background_color=(color)this.GetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR); // Цвет фона элемента

   this.m_struct_obj.tab_alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);                              // Местоположение вкладок внутри элемента управления
   this.m_struct_obj.alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_ALIGNMENT);                                      // Местоположение объекта внутри элемента управления
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj);   // Имя объекта-графического элемента
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res);   // Имя графического ресурса
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_TEXT),this.m_struct_obj.text);           // Текст графического элемента
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION),this.m_struct_obj.descript);// Описание графического элемента
   //--- Сохранение структуры в uchar-массив
   ::ResetLastError();
   if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт объект из структуры                                      |
//+------------------------------------------------------------------+
void CGCnvElement::StructToObject(void)
  {
//--- Сохранение целочисленных свойств
   this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id);                                    // Идентификатор элемента
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type);                                // Тип графического элемента
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_BELONG,this.m_struct_obj.belong);                            // Принадлежность графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number);                               // Номер элемента в списке
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,this.m_struct_obj.visible_area_h);       // Высота области видимости
   this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,this.m_struct_obj.displayed);                      // Флаг отображения не скрытого элемента управления
   this.SetProperty(CANV_ELEMENT_PROP_ZORDER,this.m_struct_obj.zorder);                            // Приоритет графического объекта на получение события нажатия мышки на графике
   this.SetProperty(CANV_ELEMENT_PROP_ENABLED,this.m_struct_obj.enabled);                          // Флаг доступности элемента
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,this.m_struct_obj.fore_color);                    // Цвет текста по умолчанию для всех объектов элемента управления
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY,this.m_struct_obj.fore_color_opacity);    // Непрозрачность цвета текста по умолчанию для всех объектов элемента управления

   this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,this.m_struct_obj.tab_alignment);                             // Местоположение вкладок внутри элемента управления
   this.SetProperty(CANV_ELEMENT_PROP_ALIGNMENT,this.m_struct_obj.alignment);                                     // Местоположение объекта внутри элемента управления
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj));   // Имя объекта-графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res));   // Имя графического ресурса
   this.SetProperty(CANV_ELEMENT_PROP_TEXT,::CharArrayToString(this.m_struct_obj.text));           // Текст графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,::CharArrayToString(this.m_struct_obj.descript));// Описание графического элемента
  }
//+------------------------------------------------------------------+


В классе базового WinForms-объекта в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh в методе, возвращающем описание целочисленного свойства элемента, впишем блок кода для возврата описания нового свойства объекта:

//+------------------------------------------------------------------+
//| Возвращает описание целочисленного свойства элемента             |
//+------------------------------------------------------------------+
string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER property,bool only_prop=false)
  {
   return
     (
      property==CANV_ELEMENT_PROP_ID                           ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :

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

      property==CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT          ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_DISPLAYED                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_DISPLAYED)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_GROUP                        ?  CMessage::Text(MSG_GRAPH_OBJ_PROP_GROUP)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :

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

      property==CANV_ELEMENT_PROP_ALIGNMENT                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Если в метод передано это свойство, то, в зависимости от флага only_prop создаётся текстовое сообщение — либо просто наименование свойства (only_prop = true), либо вместе с установленным свойству значением (only_prop = false).


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

В файле класса объекта-формы \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 bool      SendEvent(const long chart_id,const ushort event_id);

public:


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

//+------------------------------------------------------------------+
//| Показывает форму                                                 |
//+------------------------------------------------------------------+
void CForm::Show(void)
  {
//--- Если элемент не должен показываться (скрыт внутри другого элемента управления) - уходим
   if(!this.Displayed())
      return;
//--- Если у объекта есть тень - отобразим её
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Отобразим главную форму
   CGCnvElement::Show();
//--- В цикле по всем привязанным графическим объектам
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- получим очередной графический элемент
      CGCnvElement *element=this.m_list_elements.At(i);
      if(element==NULL)
         continue;
      //--- и отобразим его
      element.Show();
     }
//--- Обновим форму
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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


За пределами тела класса напишем реализацию метода, отправляющего сообщение о событии:

//+------------------------------------------------------------------+
//| Отправляет сообщение о событии                                   |
//+------------------------------------------------------------------+
bool CForm::SendEvent(const long chart_id,const ushort event_id)
  {
   //--- Создаём событие:
   //--- Получаем базовый и главный объекты
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   //--- находим имена главного и базового объектов
   string name_main=(main!=NULL ? main.Name() : this.IsMain() ? this.Name() : "Lost name of object");
   string name_base=(base!=NULL ? base.Name() : "Lost name of object");
   ENUM_GRAPH_ELEMENT_TYPE base_base_type=(base!=NULL ? base.GetBase().TypeGraphElement() : this.TypeGraphElement());
   //--- в long-параметре события передаём идентификатор объекта
   //--- в double-параметре события передаём тип объекта
   //--- в string-параметре события передаём имена главного, базового и текущего объектов, разделённые символом ";"
   long lp=this.ID();
   double dp=base_base_type;
   string sp=::StringSubstr(name_main,::StringLen(this.NamePrefix()))+";"+
             ::StringSubstr(name_base,::StringLen(this.NamePrefix()))+";"+
             ::StringSubstr(this.Name(),::StringLen(this.NamePrefix()));
   //--- Отправляем на график управляющей программы событие щелчка по элементу управления
   bool res=true;
   ::ResetLastError();
   res=::EventChartCustom(chart_id,event_id,lp,dp,sp);
   if(res)
      return true;
   ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_ENQUEUE_EVENT),". ",CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError()));
   return false;
  }
//+------------------------------------------------------------------+

Здесь мы получаем указатели на главный и базовый объекты и получаем их имена. При этом, если указатель на главный объект имеет значение NULL, то скорее всего это и есть главный объект. Для этого проверяем так ли это и, если да, то используем имя текущего объекта. Если указатель по какой-либо причине не получен, то используем для имени строку "Lost name of object" — чтобы можно было её контролировать.

Затем нам нужно узнать тип базового объекта, к которому присоединён базовый объект данного объекта (т.е. из базового объекта получить его базовый, а из него — его тип), и все полученные данные записать в переменные, отправляемые в сообщении о событии. В lparam будем отправлять идентификатор текущего объекта, в dparam — тип базового объекта, к которому присоединён базовый объект текущего объекта, а в sparam будем передавать строку, в которой через разделитель ";" записаны имена трёх объектов — главного, базового и текущего. При получении события мы по этим данным сможем определить точно из какого объекта пришло сообщение о событии.

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

Теперь в обработчики событий WinForms-объектов добавим отправку сообщений о событиях.

В файле класса объекта-кнопки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, в обработчике события "Курсор в пределах активной области, отжата кнопка мышки (левая)" впишем отправку сообщений о событии:

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если кнопка мышки отпущена за пределами элемента - это отказ от взаимодействия с элементом
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      //--- Если это простая кнопка - устанавливаем изначальный цвет фона и текста
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      //--- Если это кнопка-переключатель - устанавливаем изначальный цвет фона и текста в зависимости от того нажата кнопка или нет
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);
         this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
        }
      //--- Устанавливаем изначальный цвет рамки
      this.SetBorderColor(this.BorderColorInit(),false);
      //--- Отправляем событие:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- Если кнопка мышки отпущена в пределах элемента - это щелчок по элементу управления
   else
     {
      //--- Если это простая кнопка - устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной"
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.ForeColorMouseOver(),false);
        }
      //--- Если это кнопка-переключатель -
      else
        {
         //--- если кнопка не работает в группе - устанавливаем её состояние на противоположное,
         if(!this.GroupButtonFlag())
            this.SetState(!this.State());
         //--- иначе - если кнопка ещё не нажата - устанавливаем её в нажатое состояние
         else if(!this.State())
            this.SetState(true);
         //--- устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной" в зависимости от того нажата кнопка или нет
         this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
        }
      
      //--- Отправляем событие:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK);
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Устанавливаем цвет рамки для состояния "Курсор мышки над активной зоной"
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
//--- Перерисовываем объект
   this.Redraw(false);
  }
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\RadioButton.mqh:

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


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh:

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


Уже после публикации прошлой статьи, заметил, что перестали самостоятельно компилироваться два файла классов объектов-кнопок со стрелками влево-вправо и вверх-вниз. Они могут быть скомпилированы только в составе библиотеки — при компиляции главного файла библиотеки Engine.mqh, а вот самостоятельно — нет. Это непорядок. Для исправления ситуации нужно в файлах этих объектов изменить список подключаемых файлов.
Ранее у нас был подключен файл объекта-панели:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+

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

Для файла объекта-кнопки со стрелками вверх-вниз \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:

//+------------------------------------------------------------------+
//|                                               ArrowUpDownBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ArrowUpButton.mqh"
#include "..\Helpers\ArrowDownButton.mqh"
//+------------------------------------------------------------------+


Для файла объекта-кнопки со стрелками влево-вправо \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:

//+------------------------------------------------------------------+
//|                                            ArrowLeftRightBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ArrowLeftButton.mqh"
#include "..\Helpers\ArrowRightButton.mqh"
//+------------------------------------------------------------------+

Теперь оба файла будут нормально компилироваться как самостоятельно, так и в составе библиотеки.


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

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

      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Создаём событие:
      //--- Получаем базовый и главный объекты
      CWinFormBase *base=this.GetBase();
      CWinFormBase *main=this.GetMain();
      //--- в long-параметре события будем передавать строку, в double-параметре - колонку расположения заголовка вкладки
      long lp=this.Row();
      double dp=this.Column();
      //--- в string-параметре события будем передавать имена главного и базового объектов, разделённые символом ";"
      string name_main=(main!=NULL ? main.Name() : "");
      string name_base=(base!=NULL ? base.Name() : "");
      string sp=name_main+";"+name_base;
      //--- Отправляем на график управляющей программы событие выбора вкладки
      ::EventChartCustom(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT,lp,dp,sp);
      //--- Устанавливаем цвет рамки для состояния "Курсор мышки над активной зоной"
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CTabHeader::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если кнопка мышки отпущена за пределами элемента - это отказ от взаимодействия с элементом
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      //--- Если это простая кнопка - устанавливаем изначальный цвет фона и текста
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      //--- Если это кнопка-переключатель - устанавливаем изначальный цвет фона и текста в зависимости от того нажата кнопка или нет
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);
         this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
        }
      //--- Устанавливаем изначальный цвет рамки
      this.SetBorderColor(this.BorderColorInit(),false);
      //--- Отправляем событие:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- Если кнопка мышки отпущена в пределах элемента - это щелчок по элементу управления
   else
     {
      //--- Если это простая кнопка - устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной"
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.ForeColorMouseOver(),false);
        }
      //--- Если это кнопка-переключатель -
      else
        {
         //--- если кнопка не работает в группе - устанавливаем её состояние на противоположное,
         if(!this.GroupButtonFlag())
            this.SetState(!this.State());
         //--- иначе - если кнопка ещё не нажата - устанавливаем её в нажатое состояние
         else if(!this.State())
            this.SetState(true);
         //--- устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной" в зависимости от того нажата кнопка или нет
         this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
         
         //--- Получаем объект-поле, соответствующий этому заголовку
         CWinFormBase *field=this.GetFieldObj();
         if(field!=NULL)
           {
            //--- Отображаем поле, выводим его на передний план, рисуем рамку и обрезаем лишнее
            field.Show();
            field.BringToTop();
            field.DrawFrame();
            field.Crop();
           }
        }
      //--- Отправляем событие:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT);
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Устанавливаем цвет рамки для состояния "Курсор мышки над активной зоной"
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
//--- Перерисовываем объект и график
   this.Redraw(true);
  }
//+------------------------------------------------------------------+


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

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

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

//+------------------------------------------------------------------+
//| Обрезает изображение, очерченное рассчитываемой                  |
//| прямоугольной областью видимости                                 |
//+------------------------------------------------------------------+
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 : 0);
   int dec_size_vis=(this.State() ? 0 : 2);
//--- Рассчитываем границы области контейнера, внутри которой объект полностью виден
   int top=fmax(base.CoordY()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP),base.CoordYVisibleArea())+dec_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)-dec_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())+dec_size_vis;
   int right=fmin(base.RightEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_RIGHT),base.RightEdgeVisibleArea()+1)-add_size_lr;
//--- Рассчитываем величины областей сверху, снизу, слева и справа, на которые объект выходит
//--- за пределы границ области контейнера, внутри которой объект полностью виден
   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);
  }
//+------------------------------------------------------------------+

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


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

//--- Возвращает список (1) заголовков, (2) полей вкладок, указатель на объекты кнопки (3) "вверх-вниз", (4) "влево-вправо"
   CArrayObj        *GetListHeaders(void)          { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);        }
   CArrayObj        *GetListFields(void)           { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);         }
   CArrowUpDownBox  *GetArrUpDownBox(void)         { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0); }
   CArrowLeftRightBox *GetArrLeftRightBox(void)    { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,0); }
   
//--- Возвращает указатель на (1) последний в списке, (2) первый видимый заголовок вкладки
   CTabHeader       *GetLastHeader(void)           { return this.GetTabHeader(this.TabPages()-1);                                }
   CTabHeader       *GetFirstVisibleHeader(void);
//--- Устанавливает вкладку выбранной
   void              SetSelected(const int index);
//--- Устанавливает вкладку не выбранной
   void              SetUnselected(const int index);
//--- Устанавливает номер выбранной вкладки
   void              SetSelectedTabPageNum(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
//--- Располагает заголовки вкладок в соответствии с установленными режимами
   void              ArrangeTabHeaders(void);
//--- Располагает заголовки вкладок (1) сверху, (2) снизу, (3) слева, (4) справа
   void              ArrangeTabHeadersTop(void);
   void              ArrangeTabHeadersBottom(void);
   void              ArrangeTabHeadersLeft(void);
   void              ArrangeTabHeadersRight(void);
//--- Растягивает заголовки вкладок на размер элемента управления
   void              StretchHeaders(void);
//--- Растягивает заголовки вкладок по (1) ширине, высоте элемента управления при расположении (2) слева, (3) справа
   void              StretchHeadersByWidth(void);
   void              StretchHeadersByHeightLeft(void);
   void              StretchHeadersByHeightRight(void);
//--- Прокручивает строку заголовков (1) влево, (2) вправо, (3) вверх при расположении заголовков слева, (4) вниз, (3) вверх, (4) вниз
   void              ScrollHeadersRowToLeft(void);
   void              ScrollHeadersRowToRight(void);
//--- Прокручивает строку заголовков при их расположении слева (1) вверх, (2) вниз
   void              ScrollHeadersRowLeftToUp(void);
   void              ScrollHeadersRowLeftToDown(void);
//--- Прокручивает строку заголовков при их расположении справа (1) вверх, (2) вниз
   void              ScrollHeadersRowRightToUp(void);
   void              ScrollHeadersRowRightToDown(void);
public:

и удалим публичный метод

//--- Показывает элемент управления
   virtual void      Show(void);
//--- Смещает строку заголовков
   void              ShiftHeadersRow(const int selected);
//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Конструктор

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

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

  • При расположении заголовков сверху или снизу — два метода для прокрутки влево и прокрутки вправо.
  • При расположении заголовков слева — два метода для прокрутки влево и прокрутки вправо. 
  • При расположении заголовков справа — два метода для прокрутки влево и прокрутки вправо.

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

//+------------------------------------------------------------------+
//| Создаёт указанное количество вкладок                             |
//+------------------------------------------------------------------+
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);

//--- В цикле по количеству вкладок

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

//--- Создаём объекты-кнопки влево-вправо и вверх-вниз
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,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.GetMain());
      box_lr.SetBase(this.GetObject());
      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.GetMain());
         lb.SetBase(box_lr);
        }
      CArrowRightButton *rb=box_lr.GetArrowRightButton();
      if(rb!=NULL)
        {
         rb.SetMain(this.GetMain());
         rb.SetBase(box_lr);
        }
     }
//---
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetMain(this.GetMain());
      box_ud.SetBase(this.GetObject());
      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.GetMain());
         db.SetBase(box_ud);
        }
      CArrowUpButton *ub=box_ud.GetArrowUpButton();
      if(ub!=NULL)
        {
         ub.SetMain(this.GetMain());
         ub.SetBase(box_ud);
        }
     }

//--- Выстраиваем все заголовки в соответствии с установленными режимами их отображения и выбираем указанную вкладку
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Показывает элемент управления                                    |
//+------------------------------------------------------------------+
void CTabControl::Show(void)
  {
//--- Если элемент не должен показываться (скрыт внутри другого элемента управления) - уходим
   if(!this.Displayed())
      return;
//--- Получаем список всех заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Если у объекта есть тень - отобразим её
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Отобразим контейнер
   CGCnvElement::Show();
//--- Перенесём все элементы объекта на передний план
   this.BringToTop();
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Возвращает указатель на первый видимый заголовок                 |
//+------------------------------------------------------------------+
CTabHeader *CTabControl::GetFirstVisibleHeader(void)
  {
   for(int i=0;i<this.TabPages();i++)
     {
      CTabHeader *obj=this.GetTabHeader(i);
      if(obj==NULL)
         continue;
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           if(obj.CoordX()==this.CoordXWorkspace()+(obj.State() ? 0 : 2))
              return obj;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT  :
           if(obj.BottomEdge()==this.BottomEdgeWorkspace()+(obj.State() ? 0 : -2))
              return obj;
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT  :
           if(obj.CoordY()==this.CoordYWorkspace()+(obj.State() ? 0 : 2))
              return obj;
           break;
         default:
           break;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

Первый видимый заголовок — это тот заголовок, который находится слева при расположении заголовков сверху/снизу, либо снизу — при расположении заголовков слева, либо сверху — при расположении заголовков справа элемента управления. Для поиска этого крайнего заголовка нам нужно в цикле по всем заголовкам объекта проверить координаты его расположения в соответствии с местом расположения строки заголовков. Для расположения сверху, заголовок должен располагаться в начальной координате X контейнера. При этом, если заголовок не выбран, то его начальная координата смещена вправо на два пикселя. Аналогично и для иного расположения строки заголовков.
Метод в цикле ищет совпадение координат объекта с координатой контейнера в зависимости от места расположения строки заголовков, и возвращает указатель на найденный объект. Если ни один из заголовков не найден, метод возвращает NULL.


Метод, прокручивающий строку заголовков влево:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков влево                             |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowToLeft(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Если первый видимый заголовок в выбранном состоянии - устанавливаем значение корректировки размера
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Получаем указатель на последний заголовок из всех
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- Если последний заголовок полностью видимый - уходим, так как смещение всех заголовков влево завершено
   if(last.RightEdge()<=this.RightEdgeWorkspace())
      return;
//--- Получаем размер смещения
   shift=first.Width()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут влево на величину shift,
      if(header.Move(header.CoordX()-shift,header.CoordY()))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок вышел за левый край
         int x=(i==selected ? 0 : 2);
         if(header.CoordX()-x<this.CoordXWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
            //--- Получаем выбранный заголовок
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Получаем поле вкладки, соответствующее выбранному заголовку
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Рисуем рамку поля
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Метод, прокручивающий строку заголовков вправо:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков вправо                            |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowToRight(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Получаем заголовок, расположенный перед первым видимым
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- Если такого заголовка нет - уходим, так как смещение заголовков вправо завершено
   if(prev==NULL)
      return;
//--- Если этот заголовок в выбранном состоянии - указываем величину корректировки размера
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Получаем размер смещения
   shift=prev.Width()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут вправо на величину shift,
      if(header.Move(header.CoordX()+shift,header.CoordY()))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок выходит за левый край
         int x=(i==selected ? 0 : 2);
         if(header.CoordX()-x<this.CoordXWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Метод, прокручивающий строку заголовков вверх при их расположении слева:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков вверх при их расположении слева   |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowLeftToUp(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Получаем заголовок, расположенный перед первым видимым
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- Если такого заголовка нет - уходим, так как смещение заголовков вверх завершено
   if(prev==NULL)
      return;
//--- Если этот заголовок в выбранном состоянии - указываем величину корректировки размера
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Получаем размер смещения
   shift=prev.Height()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут вверх на величину shift,
      if(header.Move(header.CoordX(),header.CoordY()-shift))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок выходит за нижний край
         int x=(i==selected ? 0 : 2);
         if(header.BottomEdge()+x>this.BottomEdgeWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Метод, прокручивающий строку заголовков вниз при их расположении слева:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков вниз при их расположении слева    |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowLeftToDown(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Если первый видимый заголовок в выбранном состоянии - устанавливаем значение корректировки размера
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Получаем указатель на последний заголовок из всех
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- Если последний заголовок полностью видимый - уходим, так как смещение всех заголовков вниз завершено
   if(last.CoordY()>=this.CoordYWorkspace())
      return;
//--- Получаем размер смещения
   shift=first.Height()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут вниз на величину shift,
      if(header.Move(header.CoordX(),header.CoordY()+shift))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок вышел за нижний край
         int x=(i==selected ? 0 : 2);
         if(header.BottomEdge()-x>this.BottomEdgeWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
            //--- Получаем выбранный заголовок
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Получаем поле вкладки, соответствующее выбранному заголовку
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Рисуем рамку поля
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Метод, прокручивающий строку заголовков вверх при их расположении справа:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков вверх при их расположении справа  |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowRightToUp(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Если первый видимый заголовок в выбранном состоянии - устанавливаем значение корректировки размера
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Получаем указатель на последний заголовок из всех
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- Если последний заголовок полностью видимый - уходим, так как смещение всех заголовков вверх завершено
   if(last.BottomEdge()<=this.BottomEdgeWorkspace())
      return;
//--- Получаем размер смещения
   shift=first.Height()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут вверх на величину shift,
      if(header.Move(header.CoordX(),header.CoordY()-shift))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок вышел за верхний край
         int x=(i==selected ? 0 : 2);
         if(header.CoordY()-x<this.CoordYWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
            //--- Получаем выбранный заголовок
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Получаем поле вкладки, соответствующее выбранному заголовку
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Рисуем рамку поля
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Метод, прокручивающий строку заголовков вниз при их расположении справа:

//+------------------------------------------------------------------+
//| Прокручивает строку заголовков вниз при их расположении справа   |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowRightToDown(void)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Объявляем переменные и получаем номер выбранной вкладки
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Получаем первый видимый заголовок
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Получаем заголовок, расположенный перед первым видимым
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- Если такого заголовка нет - уходим, так как смещение заголовков вниз завершено
   if(prev==NULL)
      return;
//--- Если этот заголовок в выбранном состоянии - указываем величину корректировки размера
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Получаем размер смещения
   shift=prev.Height()-correct_size;
//--- В цикле по всем заголовкам
   for(int i=0;i<this.TabPages();i++)
     {
      //--- получаем очередной заголовок
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- и, если заголовок успешно сдвинут вниз на величину shift,
      if(header.Move(header.CoordX(),header.CoordY()+shift))
        {
         //--- запоминаем его новые относительные координаты
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- Если заголовок выходит за верхний край
         int x=(i==selected ? 0 : 2);
         if(header.CoordY()-x<this.CoordYWorkspace())
           {
            //--- обрезаем его и скрываем
            header.Crop();
            header.Hide();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            header.Show();
            header.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Получаем выбранный заголовок
   CTabHeader *obj=this.GetTabHeader(selected);
//---Если заголовок размещён в видимой части элемента управления - выводим его на передний план
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

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

Обработчик событий:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTabControl::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_TAB_SELECT)
     {
      //--- Получаем заголовок выбранной вкладки
      CTabHeader *header=this.GetTabHeader(this.SelectedTabPageNum());
      if(header==NULL)
         return;
      //--- В зависимости от расположения строки заголовков
      switch(this.Alignment())
        {
         //--- Заголовки сверху/снизу
         case CANV_ELEMENT_ALIGNMENT_TOP     :
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
            //--- Если заголовок обрезан - смещаем строку заголовков влево
            if(header.RightEdge()>this.RightEdgeWorkspace())
               this.ScrollHeadersRowToLeft();
            break;
         //--- Заголовки слева
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
            //--- Если заголовок обрезан - смещаем строку заголовков вниз
            if(header.CoordY()<this.CoordYWorkspace())
               this.ScrollHeadersRowLeftToDown();
            break;
         //--- Заголовки справа
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
            //--- Если заголовок обрезан - смещаем строку заголовков вверх
            Print(DFUN,"header.BottomEdge=",header.BottomEdge(),", this.BottomEdgeWorkspace=",this.BottomEdgeWorkspace());
            if(header.BottomEdge()>this.BottomEdgeWorkspace())
               this.ScrollHeadersRowRightToUp();
            break;
         default:
           break;
        }
      
     }

//--- Если щелчок по любой кнопке прокрутки строки заголовков
   if(id>=WF_CONTROL_EVENT_CLICK_SCROLL_LEFT && id<=WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
     {
      //--- Получаем заголовок последней вкладки
      CTabHeader *header=this.GetTabHeader(this.GetListHeaders().Total()-1);
      if(header==NULL)
         return;
      int hidden=0;
      
      //--- Если щелчок по кнопке прокрутки строки заголовков со стрелкой влево
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT)
         this.ScrollHeadersRowToRight();
      
      //--- Если щелчок по кнопке прокрутки строки заголовков со стрелкой вправо
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT)
         this.ScrollHeadersRowToLeft();
      
      //--- Если щелчок по кнопке прокрутки строки заголовков со стрелкой вниз
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
        {
         //--- В зависимости от расположения строки заголовков
         switch(this.Alignment())
           {
            //--- прокручиваем заголовки вверх соответствующим методом
            case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ScrollHeadersRowLeftToUp();    break;
            case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ScrollHeadersRowRightToUp();   break;
            default: break;
           }
        }
      
      //--- Если щелчок по кнопке прокрутки строки заголовков со стрелкой вверх
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP)
        {
         //--- В зависимости от расположения строки заголовков
         switch(this.Alignment())
           {
            //--- прокручиваем заголовки вниз соответствующим методом
            case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ScrollHeadersRowLeftToDown();  break;
            case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ScrollHeadersRowRightToDown(); break;
            default: break;
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

В классе-коллекции графических элементов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, в его обработчике событий нам нужно теперь правильно обрабатывать события, получаемые от WinForms-объектов. А именно: получить три имени из строкового параметра sparam и найти базовый объект, а из него — тот, который сгенерировал событие. После его нахождения, и если этот объект принадлежит элементу управления TabControl, то вызываем обработчик событий элемента управления TabControl, отсылая в него идентификатор события.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Указатель на стандартный графический объект
   CGCnvElement  *obj_cnv=NULL;  // Указатель на объект графического элемента на канвасе
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);

//--- Обработка событий элементов управления WinForms
   if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE)
     {
      //--- Объявляем сассив имён и заносим в него имена трёх объектов, записанные в параметре sparam через разделитель ";"
      string array[];
      if(::StringSplit(sparam,::StringGetCharacter(";",0),array)!=3)
        {
         CMessage::ToLog(MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES);
         return;
        }
      //--- Получаем по имени главный объект
      CWinFormBase *main=this.GetCanvElement(array[0]);
      if(main==NULL)
         return;
      //--- Из главного объекта получаем по имени базовый объект, внутри которого было событие
      CWinFormBase *base=main.GetElementByName(array[1]);
      CWinFormBase *base_elm=NULL;
      //--- Если элемента с таким именем нет - значит это базовый объект элемента события, прикреплённый к базовому - поищем его в списке
      if(base==NULL)
        {
         //--- Получаем список всех элементов, прикреплённых к главному объекту с типом, записанным в параметре dparam
         CArrayObj *list_obj=CSelect::ByGraphCanvElementProperty(main.GetListElements(),CANV_ELEMENT_PROP_TYPE,(long)dparam,EQUAL);
         if(list_obj==NULL || list_obj.Total()==0)
            return;
         //--- В цикле по полученному списку
         for(int i=0;i<list_obj.Total();i++)
           {
            //--- получаем очередной объект
            base_elm=list_obj.At(i);
            if(base_elm==NULL)
               continue;
            //--- Если базовый объект найден, получаем из него прикреплённый объект по имени из массива array[1]
            base=base_elm.GetElementByName(array[1]);
            if(base!=NULL)
               break;
           }
        }
      //--- Если не удалось найти базовый объект - уходим
      if(base==NULL)
         return;
      //--- Из найденного базового объекта получаем по имени объект, из которого было событие
      CWinFormBase *object=base.GetElementByName(array[2]);
      if(object==NULL)
         return;

      //+------------------------------------------------------------------+
      //|  Щелчок по элементу управления                                   |
      //+------------------------------------------------------------------+
      if(idx==WF_CONTROL_EVENT_CLICK)
        {
         //--- Если в dparam записан тип элемента управления TabControl
         if((ENUM_GRAPH_ELEMENT_TYPE)dparam==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
           {
            //--- В зависимости от типа элемента, сгенерировавшего событие, указываем тип события
            int event_id=
              (object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT   ?  WF_CONTROL_EVENT_CLICK_SCROLL_LEFT  :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT  ?  WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP     ?  WF_CONTROL_EVENT_CLICK_SCROLL_UP    :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN   ?  WF_CONTROL_EVENT_CLICK_SCROLL_DOWN  :
               WF_CONTROL_EVENT_NO_EVENT);
            //--- Если базовый элемент управления получен - вызываем его обработчик событий
            if(base_elm!=NULL)
               base_elm.OnChartEvent(event_id,lparam,dparam,sparam);
           }
        }

      //+------------------------------------------------------------------+
      //|  Выбор вкладки элемента управления TabControl                    |
      //+------------------------------------------------------------------+
      if(idx==WF_CONTROL_EVENT_TAB_SELECT)
        {
         if(base!=NULL)
            base.OnChartEvent(idx,lparam,dparam,sparam);
        }
     }
//--- Обработка событий переименования и щелчка по стандартному графическому объекту
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Рассчитаем идентификатор графика

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

Здесь тоже вся логика полностью расписана в коде и, надеюсь, в дополнительных пояснениях не нуждается.

Теперь всё готово для проверки того, что мы тут сегодня сделали.


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

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

Единственное отличие советника от прошлой версии — создадим 15 вкладок в элементе управления TabControl:

         //--- Создадим элемент управления 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,60,20,80,20,clrDodgerBlue,255,true,false);
               CLabel *label=tc.GetTabElement(j,0);
               if(label==NULL)
                  continue;
               label.SetText("TabPage"+string(j+1));
              }
           }

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

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


Как видим, всё, что хотели сегодня сделать — всё работает, как и задумывалось.

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

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

Этим займёмся уже в последующих статьях наряду с разработкой нового WinForms-объекта.


Что дальше

В следующей статье начнём разработку WinForms-объекта SplitContainer.

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

К содержанию

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

 
DoEasy. Элементы управления (Часть 13): Оптимизация взаимодействия WinForms-объектов с мышкой, начало разработки WinForms-объекта TabControl
DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl
DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками 
DoEasy. Элементы управления (Часть 16): WinForms-объект TabControl — несколько рядов заголовков вкладок, режим растягивания заголовков под размеры контейнера
DoEasy. Элементы управления (Часть 17): Отсечение невидимых участков объектов, вспомогательные WinForms-объекты кнопки со стрелками
DoEasy. Элементы управления (Часть 18): Готовим функционал для прокрутки вкладок в TabControl

Прикрепленные файлы |
MQL5.zip (4544.18 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Ivan Butko
Ivan Butko | 24 сент. 2022 в 04:37
Ничего себе. Удобная штука. Такие перспективы использования. 
Надо другие статьи из цикла глянуть. 
Автору спасибо
Aliaksandr Hryshyn
Aliaksandr Hryshyn | 24 сент. 2022 в 16:36

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

Как однозначно получить созданный элемент? Если у нас до этого уже были элементы типа GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL, то использование индекса 0 будет не верным.

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);

Дальше... Получили id заголовка интересующей вкладки:

int my_id=-1;
...
CTabHeader *th=tc.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,4);
my_id=th.ID();

Ловим событие изменения активной вкладки, точнее, событие клика на заголовке. При клике на активной вкладке событие всёравно генерируется.

if((id-CHARTEVENT_CUSTOM==WF_CONTROL_EVENT_TAB_SELECT)&&(lparam==my_id))
     {
      Print(sparam);
     }

А теперь как получить объект активного поля? Это ведь CTabField?

Как узнать родительский объект?

Artyom Trishkin
Artyom Trishkin | 24 сент. 2022 в 19:04
Aliaksandr Hryshyn #:

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

Как однозначно получить созданный элемент? Если у нас до этого уже были элементы типа GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL, то использование индекса 0 будет не верным.

Дальше... Получили id заголовка интересующей вкладки:

Ловим событие изменения активной вкладки, точнее, событие клика на заголовке. При клике на активной вкладке событие всёравно генерируется.

А теперь как получить объект активного поля? Это ведь CTabField?

Как узнать родительский объект?

1.   Индекс 0 используется для самого первого созданного прикреплённого объекта к элементу. Индекс 1 - для второго, индекс 2 - для третьего, и т.д.

2.   Можно получить поле из полученного заголовка по интересующему индексу:

         //--- Создадим элемент управления 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)
           {
            CTabHeader *th=tc.GetTabHeader(index);
            CTabField  *tf=th.GetFieldObj();
           }

2.1  Можно получить поле вкладки из объекта TabControl по интересующему индексу:

         //--- Создадим элемент управления 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)
           {
            CTabField  *tf=tc.GetTabField(index);
           }

3.   Не совсем понял вопрос. Если нужно узнать к какому объекту привязан текущий, то должно так работать:

      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)
           {
            CTabField  *tf=tc.GetTabField(index);  // Получаем поле вкладки из элемента TabControl tc
            CWinFormBase *base=tf.GetBase();       // Узнаём базовый объект для поля вкладки - должен быть TabControl tc
            CWinFormBase *main=tf.GetMain();       // Узнаём главный объект для поля вкладки - должен быть CPanel pnl
           }

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

Разработка торгового советника с нуля (Часть 22): Новая система ордеров (V) Разработка торгового советника с нуля (Часть 22): Новая система ордеров (V)
Сегодня мы продолжим разработку новой системы ордеров. Внедрить новую систему совсем непросто: мы часто сталкиваемся с проблемами, которые сильно усложняют процесс. Когда эти проблемы появляются, нам приходится останавливаться и заново анализировать направление, по которому мы движемся.
Нейросети — это просто (Часть 30): Генетические алгоритмы Нейросети — это просто (Часть 30): Генетические алгоритмы
Сегодня я хочу познакомить Вас с немного иным методом обучения. Можно сказать, что он заимствован из теории эволюции Дарвина. Наверное, он менее контролируем в сравнении с рассмотренными ранее методами. Но при этом позволяет обучать и недифференцируемые модели.
Разработка торгового советника с нуля (Часть 23): Новая система ордеров (VI) Разработка торгового советника с нуля (Часть 23): Новая система ордеров (VI)
Мы сделаем систему ордеров более гибкой. Здесь я покажу вам, как и где внести изменения в код, чтобы делать его более гибким, что позволит нам намного быстрее изменять лимиты позиций.
Работа с матрицами и векторами в MQL5 Работа с матрицами и векторами в MQL5
Для решения математических задач в MQL5 были добавлены матрицы и векторы. Новые типы имеют встроенные методы для написания краткого и понятного кода, который близок к математической записи. Массивы — это хорошо, но матрицы во многих случаях лучше.