preview
DoEasy. Элементы управления (Часть 33): вертикальный "ScrollBar"

DoEasy. Элементы управления (Часть 33): вертикальный "ScrollBar"

MetaTrader 5Примеры | 21 февраля 2024, 15:07
504 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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


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

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

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

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

В файле библиотеки \MQL5\Include\DoEasy\Services\DELib.mqh, в самом его конце напишем функцию:

//+------------------------------------------------------------------+
//| Получает время открытия виртуального бара по входному времени и  |
//| таймфрейму вне зависимости от существования реального бара.      |
//| корректно считает только до 28.02.2100                           |
//| не является заменой iBarShift!!! Не зависит от истории баров.    |
//| https://www.mql5.com/ru/forum/170952/page234#comment_50523898    |
//+------------------------------------------------------------------+
datetime GetStartTimeOfBarFast(const ENUM_TIMEFRAMES timeframe, const datetime time)
  {
   ENUM_TIMEFRAMES tf=(timeframe==PERIOD_CURRENT ? _Period : timeframe);

   int ts=0;
   if(tf<PERIOD_MN1)
     {
      ushort i_tf=ushort(tf);
      uchar _i=uchar(i_tf>>14);
      int n=i_tf & 0x0FFF;
      ts=(_i==0 ? n*60 : _i==1 ? n*60*60 : 60*60*24*7);
     }
   if(tf<PERIOD_W1)
      return time-time % ts;
   if(tf==PERIOD_W1)
      return time-(time+4*24*60*60) % ts;
   else // Period MN1
     {
      static int dm[12] = {0,31,61,92,122,153,184, 214, 245, 275, 306, 337};
      static int last_days = 0;
      static datetime last_result = 0;
      int days = int(time/(24*60*60));
      if(last_days!=days)
        {
         last_days = days;
         int d1 = (days+306+365)%1461;
         int y = d1/365;
         datetime t1 = time - time % (24*60*60) - d1*24*60*60;
         int m = 0;
         if(d1==1460)
           {
            m=11;
            y--;
           };
         int d = d1-y*365+1;
         if(d!=31)
            if(d==276)
               m = 9;
            else
               m = int(d/30.68);
         if(m<0 || m>11)
            return WRONG_VALUE;
         last_result = t1+y*365*24*60*60+dm[m]*24*60*60;
        }
      return last_result;
     }
  }
//+------------------------------------------------------------------+

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

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

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

//--- Изменяет светлоту (1) ARGB, (2) COLOR цвета на указанную величину
   uint              ChangeColorLightness(const uint clr,const double change_value);
   color             ChangeColorLightness(const color colour,const double change_value);
//--- Изменяет насыщенность (1) ARGB, (2) COLOR цвета на указанную величину
   uint              ChangeColorSaturation(const uint clr,const double change_value);
   color             ChangeColorSaturation(const color colour,const double change_value);
//--- Изменяет цветовую составляющую цвета RGB-Color
   color             ChangeRGBComponents(color clr,const uchar R,const uchar G,const uchar B);
   

За пределами тела класса напишем его реализацию:

//+------------------------------------------------------------------+
//| Изменяет цветовую составляющую цвета RGB-Color                   |
//+------------------------------------------------------------------+
color CGCnvElement::ChangeRGBComponents(color clr,const uchar R,const uchar G,const uchar B)
  {
   double r=CColors::GetR(clr)+R;
   if(r>255)
      r=255;
   double g=CColors::GetG(clr)+G;
   if(g>255)
      g=255;
   double b=CColors::GetB(clr)+B;
   if(b>255)
      b=255;
   return CColors::RGBToColor(r,g,b);
  }
//+------------------------------------------------------------------+
//| Сохраняет изображение в массив                                   |
//+------------------------------------------------------------------+

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

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

//--- Устанавливает относительные координаты и размеры видимой области
   void              SetVisibleArea(const int x,const int y,const int w,const int h)
                       {
                        this.SetVisibleAreaX(x,false);
                        this.SetVisibleAreaY(y,false);
                        this.SetVisibleAreaWidth(w,false);
                        this.SetVisibleAreaHeight(h,false);
                       }

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

//--- Устанавливает относительные координаты и размеры видимой области
   void              SetVisibleArea(const int x,const int y,const int w,const int h,const bool only_prop)
                       {
                        this.SetVisibleAreaX(x,only_prop);
                        this.SetVisibleAreaY(y,only_prop);
                        this.SetVisibleAreaWidth(w,only_prop);
                        this.SetVisibleAreaHeight(h,only_prop);
                       }
//--- Устанавливает размеры видимой области во весь объект
   void              ResetVisibleArea(void)                    { this.SetVisibleArea(0,0,this.Width(),this.Height(),false);            }


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

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//| без обрезания и с обновлением графика по флагу                   |
//+------------------------------------------------------------------+
void CGCnvElement::EraseNoCrop(const color colour,const uchar opacity,const bool redraw=false)
  {
   color arr[1];
   arr[0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   this.Update(redraw);
  }

Метод Update() с флагом перерисовки графика здесь всегда обновляет объект после того, как он был полностью закрашен указанным в параметрах метода EraseNoCrop() цветом. Соответственно, независимо от флага redraw, объект всегда обновлялся — т.е. отображались внесённые в него изменения. От флага перерисовки зависело лишь время отображения изменений — либо сразу (если флаг установлен в true), либо с наступлением тика или иным обновлением графика (если флаг установлен в false). А так как этот метод полностью перекрашивает весь объект, то он может в любой момент быть отображён на графике в полном своём размере. В случае, если этот объект должен быть обрезан под размеры своего родительского объекта, к которому он прикреплён, то вот эта перерисовка и вызывала неприятные "моргания" невидимой части объекта, так как обрезание невидимой части всегда выполняется после вызова этого метода.
Теперь всё исправлено и морганий невидимой части объекта больше нет:

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//| без обрезания и с обновлением графика по флагу                   |
//+------------------------------------------------------------------+
void CGCnvElement::EraseNoCrop(const color colour,const uchar opacity,const bool redraw=false)
  {
   color arr[1];
   arr[0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   if(redraw)
      this.Update(redraw);
  }

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

Теперь в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\BarProgressBar.mqh в обработчике таймера поправим вызов метода SetVisibleArea(), указав требуемый теперь флаг:

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

//--- Если объект находится в обычном состоянии (скрыт)
   if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_NORMAL)
     {
      //--- задаём объекту состояние ожидания плавного появления (здесь - смещения по полосе прогресса),
      //--- задаём длительность ожидания и устанавливаем время отсчёта
      glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_IN);
      this.m_pause.SetWaitingMSC(this.ShowDelay());
      this.m_pause.SetTimeBegin();
      //--- Если правый край объекта-блика правее левого края объекта-полосы прогресса
      if(glare.RightEdge()>=this.CoordX())
        {
         //--- Скрываем объект-блик и сдвигаем его за правый край полосы прогресса
         glare.Hide();
         if(glare.Move(this.CoordX()-glare.Width(),this.CoordY()))
           {
            //--- Устанавливаем относительные координаты объекта-блика
            glare.SetCoordXRelative(glare.CoordX()-this.CoordX());
            glare.SetCoordYRelative(glare.CoordY()-this.CoordY());
            //--- и его область видимости размером с весь объект
            glare.SetVisibleArea(0,0,glare.Width(),glare.Height(),false);
           }
        }
      return;
     }

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


Практически каждый объект библиотеки имеет в своём составе объект управления графикой, позволяющий динамически создавать графические объекты-формы. Добавим методы, позволяющие создавать некоторые стандартные графические объекты. В файле класса объекта управления графическими объектами \MQL5\Include\DoEasy\Objects\Graph\GraphElmControl.mqh в публичной секции объявим новые методы для рисования трендовых линий и стрелок:

public:
//--- Возвращает себя
   CGraphElmControl *GetObject(void)                  { return &this;               }
//--- Устанавливает тип объекта, для которого строится графика
   void              SetTypeNode(const int type_node) { this.m_type_node=type_node; }
   
//--- Создаёт объект-форму
   CForm            *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h);
   CForm            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h);
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h);

//--- Создаёт стандартный графический объект-трендовую линию
   bool              CreateTrendLine(const long chart_id,const string name,const int subwindow,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID);
   bool              CreateTrendLine(const string name,const int subwindow,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID);
   bool              CreateTrendLine(const string name,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID);

//--- Создаёт стандартный графический объект-стрелку
   bool              CreateArrow(const long chart_id,const string name,const int subwindow,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1);
   bool              CreateArrow(const string name,const int subwindow,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1);
   bool              CreateArrow(const string name,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1);

//--- Конструкторы
                     CGraphElmControl(){ this.m_type=OBJECT_DE_TYPE_GELEMENT_CONTROL; }
                     CGraphElmControl(int type_node);
  };


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

//+------------------------------------------------------------------+
//| Класс управления графическими элементами                         |
//+------------------------------------------------------------------+
class CGraphElmControl : public CObject
  {
private:
   int               m_type;                          // Тип объекта
   int               m_type_node;                     // Тип объекта, для которого строится графика
//--- Устанавливает общие параметры для стандартных графических объектов
   void              SetCommonParamsStdGraphObj(const long chart_id,const string name);
public:
//--- Возвращает себя
   CGraphElmControl *GetObject(void)                  { return &this;               }

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

//+------------------------------------------------------------------+
//|Устанавливает общие параметры для стандартных графических объектов|
//+------------------------------------------------------------------+
void CGraphElmControl::SetCommonParamsStdGraphObj(const long chart_id,const string name)
  {
   ::ObjectSetInteger(chart_id,name,OBJPROP_HIDDEN,true);
   ::ObjectSetInteger(chart_id,name,OBJPROP_SELECTED,false);
   ::ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false);
   ::ObjectSetInteger(chart_id,name,OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
  }


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

//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-трендовую линию           |
//| на указанном чарте в заданном подокне                            |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateTrendLine(const long chart_id,const string name,const int subwindow,
                                       const datetime time1,const double price1,
                                       const datetime time2,const double price2,
                                       color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
  {
   if(!CreateNewStdGraphObject(chart_id,name,OBJ_TREND,subwindow,time1,price1,time2,price2))
     {
      ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_STD_GRAPH_OBJ),": ",StdGraphObjectTypeDescription(OBJ_TREND));
      return false;
     }
   this.SetCommonParamsStdGraphObj(chart_id,name);
   ::ObjectSetInteger(chart_id,name,OBJPROP_COLOR,clr);
   ::ObjectSetInteger(chart_id,name,OBJPROP_WIDTH,width);
   ::ObjectSetInteger(chart_id,name,OBJPROP_STYLE,style);
   return true;
  }
//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-трендовую линию           |
//| на текущем чарте в заданном подокне                              |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateTrendLine(const string name,const int subwindow,
                                       const datetime time1,const double price1,
                                       const datetime time2,const double price2,
                                       color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
  {
   return this.CreateTrendLine(::ChartID(),name,subwindow,time1,price1,time2,price2,clr,width,style);
  }
//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-трендовую линию           |
//| на текущем чарте в главном окне                                  |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateTrendLine(const string name,
                                       const datetime time1,const double price1,
                                       const datetime time2,const double price2,
                                       color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
  {
   return this.CreateTrendLine(::ChartID(),name,0,time1,price1,time2,price2,clr,width,style);
  }
//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-стрелку                   |
//| на указанном чарте в заданном подокне                            |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateArrow(const long chart_id,const string name,const int subwindow,
                                   const datetime time1,const double price1,
                                   color clr,uchar arrow_code,int width=1)
  {
   if(!CreateNewStdGraphObject(chart_id,name,OBJ_ARROW,subwindow,time1,price1))
     {
      ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_STD_GRAPH_OBJ),": ",StdGraphObjectTypeDescription(OBJ_ARROW));
      return false;
     }
   this.SetCommonParamsStdGraphObj(chart_id,name);
   ::ObjectSetInteger(chart_id,name,OBJPROP_COLOR,clr);
   ::ObjectSetInteger(chart_id,name,OBJPROP_WIDTH,width);
   ::ObjectSetInteger(chart_id,name,OBJPROP_ARROWCODE,arrow_code);
   return true;
  }
//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-стрелку                   |
//| на текущем чарте в заданном подокне                              |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateArrow(const string name,const int subwindow,
                                   const datetime time1,const double price1,
                                   color clr,uchar arrow_code,int width=1)
  {
   return this.CreateArrow(::ChartID(),name,subwindow,time1,price1,clr,arrow_code,width);
  }
//+------------------------------------------------------------------+
//| Создаёт стандартный графический объект-стрелку                   |
//| на текущем чарте в главном окне                                  |
//+------------------------------------------------------------------+
bool CGraphElmControl::CreateArrow(const string name,
                                   const datetime time1,const double price1,
                                   color clr,uchar arrow_code,int width=1)
  {
   return this.CreateArrow(::ChartID(),name,0,time1,price1,clr,arrow_code,width);
  }


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

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

//+------------------------------------------------------------------+
//| Методы работы с графическими элементами                          |
//+------------------------------------------------------------------+
//--- Создаёт объект-форму на указанном графике в указанном подокне
   CForm            *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,chart_id,wnd,name,x,y,w,h);                }
//--- Создаёт объект-форму на текущем графике в указанном подокне
   CForm            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,wnd,name,x,y,w,h);                         }
//--- Создаёт объект-форму на текущем графике в главном окне
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,name,x,y,w,h);                             }
   
//--- Создаёт стандартный графический объект-трендовую линию на указанном графике в указанном подокне
   bool              CreateTrendLine(const long chart_id,const string name,const int subwindow,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
                       { return this.m_graph_elm.CreateTrendLine(chart_id,name,subwindow,time1,price1,time2,price2,clr,width,style);   }
//--- Создаёт стандартный графический объект-трендовую линию на текущем графике в указанном подокне
   bool              CreateTrendLine(const string name,const int subwindow,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
                       { return this.m_graph_elm.CreateTrendLine(::ChartID(),name,subwindow,time1,price1,time2,price2,clr,width,style);}
//--- Создаёт стандартный графический объект-трендовую линию на текущем графике в главном окне
   bool              CreateTrendLine(const string name,
                                     const datetime time1,const double price1,
                                     const datetime time2,const double price2,
                                     color clr,int width=1,ENUM_LINE_STYLE style=STYLE_SOLID)
                       { return this.m_graph_elm.CreateTrendLine(::ChartID(),name,0,time1,price1,time2,price2,clr,width,style);        }
   
//--- Создаёт стандартный графический объект-стрелку на указанном графике в указанном подокне
   bool              CreateArrow(const long chart_id,const string name,const int subwindow,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1)
                       { return this.m_graph_elm.CreateArrow(chart_id,name,subwindow,time1,price1,clr,arrow_code,width);               }
//--- Создаёт стандартный графический объект-стрелку на текущем графике в указанном подокне
   bool              CreateArrow(const string name,const int subwindow,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1)
                       { return this.m_graph_elm.CreateArrow(::ChartID(),name,subwindow,time1,price1,clr,arrow_code,width);            }
//--- Создаёт стандартный графический объект-стрелку на текущем графике в главном окне
   bool              CreateArrow(const string name,
                                 const datetime time1,const double price1,
                                 color clr,uchar arrow_code,int width=1)
                       { return this.m_graph_elm.CreateArrow(::ChartID(),name,0,time1,price1,clr,arrow_code,width);                    }
   
//--- Конструктор

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

Приступим к созданию объекта вертикальной полосы прокрутки.

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

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarThumb.mqh в обработчике события "Курсор в пределах активной области, прокручивается колёсико мышки" сделаем такие изменения:

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| прокручивается колёсико мышки                                    |
//+------------------------------------------------------------------+
void CScrollBarThumb::MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
   base.BringToTop();
   ENUM_WF_CONTROL_EVENT evn=WF_CONTROL_EVENT_NO_EVENT;
   switch(base.TypeGraphElement())
     {
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL: evn=(dparam>0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT); break;
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL  : evn=(dparam>0 ? WF_CONTROL_EVENT_CLICK_SCROLL_UP   : dparam<0 ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN  : WF_CONTROL_EVENT_NO_EVENT); break;
      default                                         : break;
     }
   base.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(base.ChartID());
  }

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

Для создания объекта вертикальной полосы прокрутки возьмём файл класса объекта горизонтальной полосы прокрутки \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarHorisontal.mqh и сохраним его под именем \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ScrollBarVertical.mqh. Так как новый класс создаётся на основе идентичного ему, то нам нужно всего лишь заменить некоторые расчёты: вместо "лево/право" в расчётах использовать "верх/низ", и такое прочее. Нет смысла описывать каждое внесённое изменение. О создании такого объекта можно почитать в соответствующей статье. Здесь же просто просмотрим файл класса целиком с уже внесёнными доработками:

//+------------------------------------------------------------------+
//|                                            ScrollBarVertical.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 "ScrollBarThumb.mqh"
#include "ArrowDownButton.mqh"
#include "ArrowUpButton.mqh"
#include "ScrollBar.mqh"
//+------------------------------------------------------------------+
//| Класс объекта CScrollBarVertical элементов управления WForms     |
//+------------------------------------------------------------------+
class CScrollBarVertical : public CScrollBar
  {
private:
//--- Создаёт объекты ArrowButton
   virtual void      CreateArrowButtons(const int width,const int height);
//--- Рассчитывает дистанцию области захвата (ползунка)
   int               CalculateThumbAreaDistance(const int thumb_size);
protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CScrollBarVertical(const ENUM_GRAPH_ELEMENT_TYPE type,
                                        CGCnvElement *main_obj,CGCnvElement *base_obj,
                                        const long chart_id,
                                        const int subwindow,
                                        const string descript,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h);
                                        
//--- Обработчик события Курсор в пределах активной области, прокручивается колёсико мышки
   virtual void      MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
public:
//--- Поддерживаемые свойства объекта (1) целочисленные, (2) вещественные, (3) строковые
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property)  { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true; }
   
//--- Возвращает кнопку со стрелкой (1) вверх, (2) вниз
   CArrowUpButton   *GetArrowButtonUp(void)     { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,0);      }
   CArrowDownButton *GetArrowButtonDown(void)   { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,0);    }

//--- Возвращает размер рабочей области ползунка
   int               BarWorkAreaSize(void);
//--- Возвращает координату начала рабочей области ползунка
   int               BarWorkAreaCoord(void);
   
//--- Устанавливает новые размеры
   virtual bool      Resize(const int w,const int h,const bool redraw);
//--- Рассчитывает и устанавливает параметры области захвата (ползунка)
   int               SetThumbParams(void);

//--- Конструктор
                     CScrollBarVertical(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                        const long chart_id,
                                        const int subwindow,
                                        const string descript,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h);
//--- Таймер
   virtual void      OnTimer(void);
//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
  };
//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CScrollBarVertical::CScrollBarVertical(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       CGCnvElement *main_obj,CGCnvElement *base_obj,
                                       const long chart_id,
                                       const int subwindow,
                                       const string descript,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h) : CScrollBar(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.CreateThumbArea();
  }
//+------------------------------------------------------------------+
//| Конструктор с указанием главного и базового объектов,            |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CScrollBarVertical::CScrollBarVertical(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                       const long chart_id,
                                       const int subwindow,
                                       const string descript,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h) : CScrollBar(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL);
   this.CreateThumbArea();
  }
//+------------------------------------------------------------------+
//| Создаёт объекты ArrowButton                                      |
//+------------------------------------------------------------------+
void CScrollBarVertical::CreateArrowButtons(const int width,const int height)
  {
//--- Устанавливаем размер кнопок, равный толщине полосы прокрутки без величины её рамки
   int size=this.Thickness()-this.BorderSizeLeft()-this.BorderSizeRight();
//--- Создаём кнопки со стрелками вверх и вниз и объект-область захвата. Размер стрелки устанавливаем равным 2
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,  0,0,size,size,this.BackgroundColor(),255,true,false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,0,this.Height()-height,size,size,this.BackgroundColor(),255,true,false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_THUMB,0,this.Height()/2-height,size,30,CLR_DEF_CONTROL_SCROLL_BAR_THUMB_COLOR,255,true,false);
   this.SetArrowSize(2);
//--- Получаем указатель на кнопку со стрелкой вверх и устанавливаем для неё цвета различных её состояний
   CArrowUpButton *bu=this.GetArrowButtonUp();
   if(bu!=NULL)
     {
      bu.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_COLOR,true);
      bu.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_DOWN);
      bu.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_OVER);
      bu.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_COLOR,true);
      bu.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_DOWN);
      bu.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_OVER);
     }
//--- Получаем указатель на кнопку со стрелкой вниз и устанавливаем для неё цвета различных её состояний
   CArrowDownButton *bd=this.GetArrowButtonDown();
   if(bd!=NULL)
     {
      bd.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_COLOR,true);
      bd.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_DOWN);
      bd.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_OVER);
      bd.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_COLOR,true);
      bd.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_DOWN);
      bd.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_OVER);
     }
//--- Получаем указатель на объект-область захвата и устанавливаем для него цвета различных его состояний
   CScrollBarThumb *th=this.GetThumb();
   if(th!=NULL)
     {
      th.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_COLOR,true);
      th.SetBorderColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_BORDER_COLOR,true);
      th.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_MOUSE_DOWN);
      th.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_MOUSE_OVER);
      th.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_COLOR,true);
      th.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_MOUSE_DOWN);
      th.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_MOUSE_OVER);
     }
  }
//+------------------------------------------------------------------+
//| Устанавливает новые размеры                                      |
//+------------------------------------------------------------------+
bool CScrollBarVertical::Resize(const int w,const int h,const bool redraw)
  {
//--- Если размеры объекта изменить не удалось - возвращаем false
   if(!CWinFormBase::Resize(w,h,redraw))
      return false;
//--- Получаем объект-кнопку со стрелкой вниз
   CArrowDownButton *bd=this.GetArrowButtonDown();
//--- Если кнопка не получена - возвращаем false
   if(bd==NULL)
      return false;
//--- Перемещаем кнопку к нижнему краю полосы прокрутки
   if(bd.Move(bd.CoordX(),this.BottomEdge()-this.BorderSizeBottom()-bd.Height()))
     {
      //--- Устанавливаем кнопке новые относительные координаты
      bd.SetCoordXRelative(bd.CoordX()-this.CoordX());
      bd.SetCoordYRelative(bd.CoordY()-this.CoordY());
     }
//--- Устанавливаем параметры ползунка
   this.SetThumbParams();
//--- Успешно
   return true;
  }
//+------------------------------------------------------------------+
//| Рассчитывает и устанавливает параметры области захвата (ползунка)|
//+------------------------------------------------------------------+
int CScrollBarVertical::SetThumbParams(void)
  {
//--- Получаем базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return 0;
//--- Получаем объект-область захвата (ползунок)
   CScrollBarThumb *thumb=this.GetThumb();
   if(thumb==NULL)
      return 0;
//--- Получаем размер видимой части по высоте внутри контейнера
   int base_h=base.HeightWorkspace();
//--- Рассчитываем полную высоту всех привязанных объектов
   int objs_h=base_h+base.OversizeTop()+base.OversizeBottom();
//--- Рассчитываем относительный размер окна видимой части
   double px=(double)base_h/double(objs_h!=0 ? objs_h : 1);
//--- Рассчитываем и корректируем размер ползунка относительно высоты его рабочей области (не меньше минимального размера)
   int thumb_size=(int)::floor(this.BarWorkAreaSize()*px);
   if(thumb_size<DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN)
      thumb_size=DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN;
   if(thumb_size>this.BarWorkAreaSize())
      thumb_size=this.BarWorkAreaSize();
//--- Рассчитываем координату ползунка и изменяем его размер под ранее рассчитанный
   int thumb_y=this.CalculateThumbAreaDistance(thumb_size);
   if(!thumb.Resize(thumb.Width(),thumb_size,true))
      return 0;
//--- Смещаем ползунок на рассчитанную координату Y
   if(thumb.Move(thumb.CoordX(),this.BarWorkAreaCoord()+thumb_y))
     {
      thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
      thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
     }
//--- Возвращаем рассчитанный размер ползунка
   return thumb_size;
  }
//+------------------------------------------------------------------+
//| Рассчитывает дистанцию области захвата (ползунка)                |
//+------------------------------------------------------------------+
int CScrollBarVertical::CalculateThumbAreaDistance(const int thumb_size)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return 0;
   double x=(double)thumb_size/(double)base.HeightWorkspace();
   return (int)::ceil((double)base.OversizeTop()*x);
  }
//+------------------------------------------------------------------+
//| Возвращает размер рабочей области ползунка                       |
//+------------------------------------------------------------------+
int CScrollBarVertical::BarWorkAreaSize(void)
  {
   CArrowUpButton  *bu=this.GetArrowButtonUp();
   CArrowDownButton *bd=this.GetArrowButtonDown();
   int y1=(bu!=NULL ? bu.BottomEdge() : this.CoordY()+this.BorderSizeTop());
   int y2=(bd!=NULL ? bd.CoordY() : this.BottomEdge()-this.BorderSizeBottom());
   return(y2-y1);
  }
//+------------------------------------------------------------------+
//| Возвращает координату начала рабочей области ползунка            |
//+------------------------------------------------------------------+
int CScrollBarVertical::BarWorkAreaCoord(void)
  {
   CArrowUpButton  *bu=this.GetArrowButtonUp();
   return(bu!=NULL ? bu.BottomEdge() : this.CoordY()+this.BorderSizeTop());
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CScrollBarVertical::OnTimer(void)
  {

  }
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CScrollBarVertical::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Корректируем смещение по Y для подокна
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
//--- Получаем указатели на управляющие объекты полосы прокрутки
   CArrowUpButton  *buttu=this.GetArrowButtonUp();
   CArrowDownButton *buttd=this.GetArrowButtonDown();
   CScrollBarThumb   *thumb=this.GetThumb();
   if(buttu==NULL || buttd==NULL || thumb==NULL)
      return;
//--- Если идентификатор события - перемещение объекта
   if(id==WF_CONTROL_EVENT_MOVING)
     {
      //--- Переносим полосу прокрутки на передний план
      this.BringToTop();
      //--- Объявляем переменные для координат области захвата
      int x=(int)lparam;
      int y=(int)dparam;
      //--- Устанавливаем координату X равной координате X элемента управления
      x=this.CoordX()+this.BorderSizeLeft();
      //--- Корректируем координату Y так, чтобы область захвата не выходила за пределы элемента управления с учётом кнопок со стрелками
      if(y<buttu.BottomEdge())
        y=buttu.BottomEdge();
      if(y>buttd.CoordY()-thumb.Height())
        y=buttd.CoordY()-thumb.Height();
      //--- Если объект-область захвата смещён на рассчитанные координаты
      if(thumb.Move(x,y,true))
        {
         //--- устанавливаем объекту его относительные координаты
         thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
         thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
        }
      //--- Получаем указатель на базовый объект
      CWinFormBase *base=this.GetBase();
      if(base!=NULL)
        {
         //--- Проверяем выход содержимого за пределы контейнера
         base.CheckForOversize();

         //--- Рассчитываем дистанцию, на которую ползунок отстоит от верхней границы полосы прокрутки (от нижней грани верхней кнопки со стрелкой)
         int distance=thumb.CoordY()-buttu.BottomEdge();
         
         //--- Объявляем переменную, хранящую величину дистанции, бывшую перед смещением ползунка
         static int distance_last=distance;
         //--- Объявляем переменную, хранящую величину в пикселях экрана, на которую был смещён ползунок
         int shift_value=0;
         
         //--- Если значения прошлой и текущей дистанции не равны (ползунок сдвинут),
         if(distance!=distance_last)
           {
            //--- рассчитаем значение, на которое сдвинут ползунок,
            shift_value=distance_last-distance;
            //--- и впишем новую дистанцию в значение прошлой дистанции для следующего рассчёта
            distance_last=distance;
           }
         
         //--- Получаем наибольшую и наименьшую координаты нижней и верхней сторон содержимого базового объекта
         int cntt_d=(int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_BOTTOM);
         int cntt_u=(int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_Y);

         //--- Получаем смещение координаты верхней стороны содержимого базового объекта
         //--- относительно начальной координаты рабочей области базового объекта
         int extu=base.CoordYWorkspace()-cntt_u;
         
         //--- Рассчитываем относительную величину нужной координаты,
         //--- где должно располагаться содержимое базового объекта, смещаемое ползунком
         double y=(double)this.HeightWorkspace()*(double)distance/double(thumb.Height()!=0 ? thumb.Height() : DBL_MIN);
         
         //--- Рассчитываем необходимую величину сдвига содержимого базового объекта по вышерассчитанной координате
         int shift_need=extu-(int)::round(y);
         
         //--- Если ползунок смещён вверх (положительное значение смещения)
         if(shift_value>0)
           {
            if(cntt_u+shift_need<=base.CoordYWorkspace())
               base.ShiftDependentObj(0,shift_need);
           }
         //--- Если ползунок смещён вниз (отрицательное значение смещения)
         if(shift_value<0)
           {
            if(cntt_d-shift_need>=base.BottomEdgeWorkspace())
               base.ShiftDependentObj(0,shift_need);
           }
         ::ChartRedraw(this.ChartID());
        }
     }
//--- Если щелчок по любой кнопке прокрутки
   if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP || id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
     {
      //--- Переносим полосу прокрутки на передний план
      this.BringToTop();
      //--- Получаем базовый объект
      CWinFormBase *base=this.GetBase();
      if(base==NULL)
         return;
      //--- Рассчитываем на сколько каждая сторона содержимого базового объекта выходит за его пределы
      base.CheckForOversize();
      //--- Получаем наибольшую и наименьшую координаты нижней и верхней сторон содержимого базового объекта
      int cntt_d=(int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_BOTTOM);
      int cntt_u=(int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_Y);
      //--- Задаём количество пикселей, на которые нужно сместить содержимое базового объекта
      int shift=(sparam!="" ? DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK : DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL);
      //--- Если щелчок по кнопке вверх
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP)
        {
         if(cntt_u+shift<=base.CoordYWorkspace())
            base.ShiftDependentObj(0,shift);
        }
      //--- Если щелчок по кнопке вниз
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
        {
         if(cntt_d-shift>=base.BottomEdgeWorkspace())
            base.ShiftDependentObj(0,-shift);
        }
      //--- Рассчитываем ширину и координаты ползунка
      this.SetThumbParams();
     }
  }
//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| прокручивается колёсико мышки                                    |
//+------------------------------------------------------------------+
void CScrollBarVertical::MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   ENUM_WF_CONTROL_EVENT evn=(dparam>0 ? WF_CONTROL_EVENT_CLICK_SCROLL_UP : dparam<0 ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN : WF_CONTROL_EVENT_NO_EVENT);
   this.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

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

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

Откроем файл \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh и в методе создания прикреплённых объектов впишем необходимые проверки и отображение полос прокрутки:

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CContainer::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h,
                                  const color colour,
                                  const uchar opacity,
                                  const bool activity,
                                  const bool redraw)
  {
//--- Если тип объекта - меньше, чем базовый WinForms-объект
   if(element_type<GRAPH_ELEMENT_TYPE_WF_BASE)
     {
      //--- сообщаем об ошибке и возвращаем false
      CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE);
      return false;
     }
//--- Если не удалось создать новый графический элемент - возвращаем false
   CWinFormBase *obj=CForm::CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
   if(obj==NULL)
      return false;
//--- Устанавливаем параметры созданному объекту
   this.SetObjParams(obj,colour);
//--- Если есть привязанные объекты
   if(this.ElementsTotal()>0)
     {
      //--- Если у панели включено автоизменение размеров - вызываем метод изменения размеров
      if(this.AutoSize())
         this.AutoSizeProcess(redraw);
      //--- При выключенном автоизменении размеров, определяем необходимость отображения полос прокрутки 
      else
        {
         if(this.CheckForOversize())
           {
            //--- Если прикреплённые объекты выходят за окно видимости слева или справа
            if(this.OversizeLeft()>0 || this.OversizeRight()>0)
              {
               CScrollBarHorisontal *sbh=this.GetScrollBarHorisontal();
               if(sbh!=NULL)
                 {
                  sbh.SetThumbParams();
                  sbh.SetDisplayed(true);
                  sbh.Show();
                 }
              }
            //--- Если прикреплённые объекты выходят за окно видимости сверху или снизу
            if(this.OversizeTop()>0 || this.OversizeBottom()>0)
              {
               CScrollBarVertical *sbv=this.GetScrollBarVertical();
               if(sbv!=NULL)
                 {
                  sbv.SetThumbParams();
                  sbv.SetDisplayed(true);
                  sbv.Show();
                 }
              }
           }
        }
     }
//--- Обрезаем созданный объект по краям видимой части контейнера
   obj.Crop();
//--- возвращаем true
   return true;
  }


Теперь необходимо подправить некоторые классы, в которых есть вызов метода перерисовки без обрезания объекта EraseNoCrop(). Нужно указать флаг false — чтобы внутри этого метода не происходило обновление объекта.

Изменения нужно внести в методы Redraw() трёх объектов в трёх файлах библиотеки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh и \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Label.mqh.

Все изменения сводятся всего лишь к указанию флага false:

//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CButton::Redraw(bool redraw)
  {
//--- Заполняем объект цветом фона
   this.EraseNoCrop(this.BackgroundColor(),this.Opacity(),false);
//--- Объявляем переменные для координат X и Y и устанавливаем их значения в зависимости от выравнивания текста
   int x=0,y=0;
   CLabel::SetTextParamsByAlign(x,y);
//--- Рисуем текст в установленных координатах объекта и точкой привязки текста и обновляем объект 
   this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.Crop();
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CCheckBox::Redraw(bool redraw)
  {
//--- Заполняем объект цветом фона с полной прозрачностью
   this.EraseNoCrop(this.BackgroundColor(),this.Opacity(),false);
//--- Устанавливаем скорректированные координаты текста относительно флажка проверки
   this.SetCorrectTextCoords();
//--- Рисуем текст и флажок выбора в установленных координатах объекта и точкой привязки и обновляем объект 
   this.Text(this.m_text_x,this.m_text_y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.ShowControlFlag(this.CheckState());
   this.Crop();
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CLabel::Redraw(bool redraw)
  {
//--- Заполняем объект цветом фона с полной прозрачностью
   this.EraseNoCrop(this.BackgroundColor(),0,false);
//--- Объявляем переменные для координат X и Y и устанавливаем их значения в зависимости от выравнивания текста
   int x=0,y=0;
   this.SetTextParamsByAlign(x,y);
//--- Рисуем текст в установленных координатах объекта и точкой привязки текста и обновляем объект 
   this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.Crop();
   this.Update(redraw);
  }

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


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

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

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

//--- Создадим требуемое количество объектов WinForms Panel
   CPanel *pnl=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      pnl=engine.CreateWFPanel("WinForms Panel"+(string)i,(i==0 ? 50 : 70),(i==0 ? 50 : 70),410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         //--- Установим значение Padding равным 4
         pnl.SetPaddingAll(3);
         //--- Установим флаги перемещаемости, автоизменения размеров и режим автоизменения из входных параметров
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
         //---
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,10,-40,pnl.WidthWorkspace()-30,pnl.HeightWorkspace()+50,clrNONE,255,true,false);
         CButton *btn=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,0);
         btn.SetText("123456789012345678901234567890");
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,40,20,60,20,clrDodgerBlue,255,false,false);
         CLabel *lbl=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_LABEL,0);
         lbl.SetText("LABEL");

Всё, других изменений не требуется.

Скомпилируем советник и запустим его на графике, предварительно указав в настройках для значения Panel Autosize значение No:


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


Что дальше

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

Все файлы прикреплены к статье и их можно самостоятельно изучить и протестировать.


К содержанию


Прикрепленные файлы |
MQL5.zip (4687.24 KB)
Добавляем пользовательскую LLM в торгового робота (Часть 1): Развертывание оборудования и среды Добавляем пользовательскую LLM в торгового робота (Часть 1): Развертывание оборудования и среды
Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти мощные модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.
Освоение ONNX: Переломный момент для MQL5-трейдеров Освоение ONNX: Переломный момент для MQL5-трейдеров
Погрузитесь в мир ONNX - мощного открытого формата для обмена моделями машинного обучения. Узнайте, как использование ONNX может произвести революцию в алгоритмической торговле на MQL5, позволяя трейдерам беспрепятственно интегрировать передовые модели искусственного интеллекта и поднять свои стратегии на новый уровень. Раскройте секреты кросс-платформенной совместимости и узнайте, как раскрыть весь потенциал ONNX в своей торговле на MQL5. Улучшите свою торговлю с помощью этого подробного руководства по ONNX.
Угловые операции для трейдеров Угловые операции для трейдеров
В этой статье будут рассмотрены угловые операции. Мы рассмотрим методы построения углов и способы их применения в трейдинге.
Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных
Продолжение серии для начинающих. Здесь мы рассмотрим, как создавать константы и переменные, записывать дату, цвета и другие полезные данные. Научимся создавать перечисления вроде дней недели или стилей линий (сплошная, пунктирная и т.д.). Переменные и выражения - это база программирования. Они обязательно есть в 99% программ, поэтому понимать их критически важно. И поэтому, если вы - новичок в программировании - прошу. Уровень знания программирования: очень базовый - в пределах моей предыдущей статьи (ссылка - в начале).