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

DoEasy. Элементы управления (Часть 16): WinForms-объект TabControl — несколько рядов заголовков вкладок, режим растягивания заголовков под размеры контейнера

MetaTrader 5Примеры | 26 августа 2022, 13:48
754 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

  • Normal — ширина вкладок устанавливается по ширине текста заголовка, по краям заголовка добавляется пространство, указанное в значениях PaddingWidth и PaddingHeight заголовка;
  • Fixed — фиксированный размер, указываемый в настройках элемента управления. Текст заголовков обрезается, если не входит в его размеры;
  • FillToRight — вкладки, умещающиеся в пределах ширины элемента управления, растягиваются на всю его ширину.

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


И мы реализовали функционал размещения вкладок сверху элемента управления в режимах Normal и Fixed.

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

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


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

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

В файле \MQL5\Include\DoEasy\Defines.mqh впишем два новых свойства в список целочисленных свойств графического элемента на канвасе и увеличим их общее количество с 90 до 92:

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

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

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


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

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

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

   SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE,                // Сортировать по режиму установки размера вкладок
   SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER,              // Сортировать по порядковому номеру вкладки
   SORT_BY_CANV_ELEMENT_TAB_PAGE_ROW,                 // Сортировать по номеру ряда вкладки
   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_CANV_ELEMENT_PROP_TAB_SIZE_MODE,               // Режим установки размера вкладок
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,             // Порядковый номер вкладки
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW,                // Номер ряда вкладки
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,             // Номер столбца вкладки
   MSG_CANV_ELEMENT_PROP_ALIGNMENT,                   // Местоположение объекта внутри элемента управления
//--- Вещественные свойства графических элементов

//--- Строковые свойства графических элементов
   MSG_CANV_ELEMENT_PROP_NAME_OBJ,                    // Имя объекта-графического элемента
   MSG_CANV_ELEMENT_PROP_NAME_RES,                    // Имя графического ресурса
   MSG_CANV_ELEMENT_PROP_TEXT,                        // Текст графического элемента
   MSG_CANV_ELEMENT_PROP_DESCRIPTION,                 // Описание графического элемента
  };
//+------------------------------------------------------------------+

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

   {"Режим установки размера вкладок","Tab Size Mode"},
   {"Порядковый номер вкладки","Tab ordinal number"},
   {"Номер ряда вкладки","Tab row number"},
   {"Номер столбца вкладки","Tab column number"},
   {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
   
//--- Строковые свойства графических элементов
   {"Имя объекта-графического элемента","The name of the graphic element object"},
   {"Имя графического ресурса","Image resource name"},
   {"Текст графического элемента","Text of the graphic element"},
   {"Описание графического элемента","Description of the graphic element"},
  };
//+---------------------------------------------------------------------+

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


При использовании класса CCanvas Стандартной Библиотеки MQL5, не всегда можно получить код ошибки, из-за которой не удалось создать графический элемент. Мы постепенно добавляем в библиотеку исправления, позволяющие понять причину, по которой элемент не был создан.

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

//+------------------------------------------------------------------+
//| Устанавливает новую ширину                                       |
//+------------------------------------------------------------------+
bool CGCnvElement::SetWidth(const int width)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width)
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
     {
      CMessage::ToLog(DFUN+this.TypeElementDescription()+": width="+(string)width+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width);
   return true;
  }
//+------------------------------------------------------------------+
//| Устанавливает новую высоту                                       |
//+------------------------------------------------------------------+
bool CGCnvElement::SetHeight(const int height)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height)
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
     {
      CMessage::ToLog(DFUN+this.TypeElementDescription()+": height="+(string)height+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height);
   return true;
  }
//+------------------------------------------------------------------+

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


Чтобы для двух новых свойств графического элемента можно было выводить их описания, нам необходимо добавить блок кода в метод, возвращающий описание целочисленного свойства элемента, в файле \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_TYPE                         ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TYPE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.TypeElementDescription()
         )  :
      
      //---...
      //---...

      property==CANV_ELEMENT_PROP_TAB_SIZE_MODE                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_ROW                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_COLUMN              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN)+
         (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))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Здесь: в зависимости от переданного в метод свойства, создаём текстовое сообщение и возвращаем его из метода.


Теперь займёмся непосредственно доработкой классов WinForms-объекта TabControl.

Объект состоит из контейнера, в котором располагаются вкладки, в свою очередь состоящие из двух вспомогательных объектов — поля вкладки и его заголовка. На поле вкладки размещаются все объекты, которые должны быть расположены на вкладке, а заголовком вкладки мы выбираем ту вкладку, которую хотим видеть, и с которой хотим работать. Заголовки вкладок у нас реализованы в классе TabHeader, унаследованном от WinForms-объекта Button в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

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

//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
private:
   int               m_width_off;                        // Ширина объекта в состоянии "не выбран"
   int               m_height_off;                       // Высота объекта в состоянии "не выбран"
   int               m_width_on;                         // Ширина объекта в состоянии "выбран"
   int               m_height_on;                        // Высота объекта в состоянии "выбран"
   int               m_col;                              // Номер колонки заголовка
   int               m_row;                              // Номер строки заголовка
//--- Подстраивает размер и расположение элемента в зависимости от состояния
   bool              WHProcessStateOn(void);
   bool              WHProcessStateOff(void);
//--- Рисует рамку заголовка в зависимости от его расположения
   virtual void      DrawFrame(void);
//--- Устанавливает строку выделенного заголовка вкладки в корректное положение, (1) сверху, (2) снизу, (3) слева, (4) справа
   void              CorrectSelectedRowTop(void);
   void              CorrectSelectedRowBottom(void);
   void              CorrectSelectedRowLeft(void);
   void              CorrectSelectedRowRight(void);
   
protected:


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

//--- Возвращает размеры элемента управления
   int               WidthOff(void)                                                    const { return this.m_width_off;          }
   int               HeightOff(void)                                                   const { return this.m_height_off;         }
   int               WidthOn(void)                                                     const { return this.m_width_on;           }
   int               HeightOn(void)                                                    const { return this.m_height_on;          }
//--- (1) Устанавливает, (2) возвращает Номер ряда вкладки
   void              SetRow(const int value)                { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW,value);            }
   int               Row(void)                        const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW);      }
//--- (1) Устанавливает, (2) возвращает Номер столбца вкладки
   void              SetColumn(const int value)             { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,value);         }
   int               Column(void)                     const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN);   }
//--- Устанавливает местоположение вкладки
   void              SetTabLocation(const int row,const int col)
                       {
                        this.SetRow(row);
                        this.SetColumn(col);
                       }


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

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

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

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

//--- В зависимости от режима задания размера заголовка
   switch(this.TabSizeMode())
     {
      //--- устанавливаем ширину и высоту для режима Normal
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- Для режима Fixed размеры остаются указанными,
      //--- а для Fill, рассчитываются в методах StretchHeaders класса TabControl
      default: break;
     }
//--- Записываем в res результаты изменения ширины и высоты


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

//--- Устанавливаем изменённые размеры для разных состояний кнопки
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }


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

//+------------------------------------------------------------------+
//| Устанавливает все размеры заголовка                              |
//+------------------------------------------------------------------+
bool CTabHeader::SetSizes(const int w,const int h)
  {
//--- Если переданные ширина или высота меньше 4 пикселей, 
//--- делаем их равными четырём пикселям
   int width=(w<4 ? 4 : w);
   int height=(h<4 ? 4 : h);
//--- В зависимости от режима задания размера заголовка
   switch(this.TabSizeMode())
     {
      //--- устанавливаем ширину и высоту для режима Normal
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- Для режима Fixed размеры остаются указанными,
      //--- а для Fill, рассчитываются в методах StretchHeaders класса TabControl
      default: break;
     }
//--- Записываем в res результаты изменения ширины и высоты
   bool res=true;
   res &=this.SetWidth(width);
   res &=this.SetHeight(height);
//--- Если ошибка изменения ширины или высоты - возвращаем false
   if(!res)
      return false;
//--- Устанавливаем изменённые размеры для разных состояний кнопки
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


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

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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "выбран" в зависимости от его расположения           |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOn(void)
  {
//--- Если не удалось установить новый размер - уходим
   if(!this.SetSizeOn())
      return false;
//--- Получаем базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return false;
//--- В зависимости от расположения заголовка
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowTop();
        //--- смещаем заголовок на два пикселя в новые координаты расположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowBottom();
        //--- смещаем заголовок на два пикселя в новые координаты расположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX()-2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowLeft();
        //--- смещаем заголовок на два пикселя в новые координаты расположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowRight();
        //--- смещаем заголовок на два пикселя в новые координаты расположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX(),this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "не выбран" в зависимости от его расположения        |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOff(void)
  {
//--- Если не удалось установить новый размер - уходим
   if(!this.SetSizeOff())
      return false;
//--- В зависимости от расположения заголовка
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX()+2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX(),this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение снизу                                     |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowBottom(void)
  {
   int row_pressed=this.Row();      // Строка выбранного заголовка
   int y_pressed=this.CoordY();     // Координата куда перенести все заголовки с Row(), равным нулю
   int y0=0;                        // Координата нулевой строки (Row == 0)
//--- Если выбрана нулевая строка - ничего делать не нужно - уходим
   if(row_pressed==0)
      return;
      
//--- Получаем объект-поле вкладки, соответствующий этому заголовку, и устанавливаем Y-координату нулевой строки
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   y0=obj.CoordY()+obj.Height();
   
//--- Получаем базовый объект (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех заголовков вкладок
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- В цикле по всем заголовкам меняем местами строки -
//--- строку выбранного заголовка устанавливаем на место нулевой, нулевую - на место строки выбранного заголовка
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это нулевая строка
      if(header.Row()==0)
        {
         //--- перемещаем заголовок на место строки выбранного
         if(header.Move(header.CoordX(),y_pressed))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой нулевой строки на место выбранной, зададим значение Row как -1
            header.SetRow(-1);
           }
        }
      //--- Если это строка нажатого заголовка
      if(header.Row()==row_pressed)
        {
         //--- перемещаем заголовок на место нулевой строки
         if(header.Move(header.CoordX(),y0))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой выбранной строки на место нулевой, зададим значение Row как -2
            header.SetRow(-2);
           }
        }
     }
//--- Устанавливаем правильные Row и Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это бывшая нулевая строка, перемещённая на место выбранной, ставим ей Row выбранной строки
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- Если это выбранная строка, перемещённая на место нулевой - ставим Row нулевой строки
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение слева                                     |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowLeft(void)
  {
   int row_pressed=this.Row();      // Строка выбранного заголовка
   int x_pressed=this.CoordX();     // Координата куда перенести все заголовки с Row(), равным нулю
   int x0=0;                        // Координата нулевой строки (Row == 0)
//--- Если выбрана нулевая строка - ничего делать не нужно - уходим
   if(row_pressed==0)
      return;
      
//--- Получаем объект-поле вкладки, соответствующий этому заголовку, и устанавливаем X-координату нулевой строки
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.CoordX()-this.Width()+2;
   
//--- Получаем базовый объект (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех заголовков вкладок
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- В цикле по всем заголовкам меняем местами строки -
//--- строку выбранного заголовка устанавливаем на место нулевой, нулевую - на место строки выбранного заголовка
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это нулевая строка
      if(header.Row()==0)
        {
         //--- перемещаем заголовок на место строки выбранного
         if(header.Move(x_pressed,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой нулевой строки на место выбранной, зададим значение Row как -1
            header.SetRow(-1);
           }
        }
      //--- Если это строка нажатого заголовка
      if(header.Row()==row_pressed)
        {
         //--- перемещаем заголовок на место нулевой строки
         if(header.Move(x0,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой выбранной строки на место нулевой, зададим значение Row как -2
            header.SetRow(-2);
           }
        }
     }
//--- Устанавливаем правильные Row и Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это бывшая нулевая строка, перемещённая на место выбранной, ставим ей Row выбранной строки
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- Если это выбранная строка, перемещённая на место нулевой - ставим Row нулевой строки
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение справа                                    |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowRight(void)
  {
   int row_pressed=this.Row();      // Строка выбранного заголовка
   int x_pressed=this.CoordX();     // Координата куда перенести все заголовки с Row(), равным нулю
   int x0=0;                        // Координата нулевой строки (Row == 0)
//--- Если выбрана нулевая строка - ничего делать не нужно - уходим
   if(row_pressed==0)
      return;
      
//--- Получаем объект-поле вкладки, соответствующий этому заголовку, и устанавливаем X-координату нулевой строки
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.RightEdge();
   
//--- Получаем базовый объект (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех заголовков вкладок
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- В цикле по всем заголовкам меняем местами строки -
//--- строку выбранного заголовка устанавливаем на место нулевой, нулевую - на место строки выбранного заголовка
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это нулевая строка
      if(header.Row()==0)
        {
         //--- перемещаем заголовок на место строки выбранного
         if(header.Move(x_pressed,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой нулевой строки на место выбранной, зададим значение Row как -1
            header.SetRow(-1);
           }
        }
      //--- Если это строка нажатого заголовка
      if(header.Row()==row_pressed)
        {
         //--- перемещаем заголовок на место нулевой строки
         if(header.Move(x0,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой выбранной строки на место нулевой, зададим значение Row как -2
            header.SetRow(-2);
           }
        }
     }
//--- Устанавливаем правильные Row и Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это бывшая нулевая строка, перемещённая на место выбранной, ставим ей Row выбранной строки
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- Если это выбранная строка, перемещённая на место нулевой - ставим Row нулевой строки
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+

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


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

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

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

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


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

//--- Располагает заголовки вкладок (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);
public:


За пределами тела класса напишем реализацию этих методов.

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

//+------------------------------------------------------------------+
//| Растягивает заголовки вкладок на размер элемента управления      |
//+------------------------------------------------------------------+
void CTabControl::StretchHeaders(void)
  {
//--- Если заголовки располагаются в один ряд - уходим
   if(!this.Multiline())
      return;
//--- В зависимости от расположения заголовков
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.StretchHeadersByWidth();
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        this.StretchHeadersByHeightLeft();
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        this.StretchHeadersByHeightRight();
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Растягивает заголовки вкладок по ширине элемента управления      |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByWidth(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- В цикле по количеству рядов заголовков
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Получаем список с номером ряда, равным индексу цикла
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Получаем ширину контейнера, количество заголовков в ряду и рассчитываем ширину каждого заголовка
      int base_size=this.Width()-4;
      int num=list_row.Total();
      int w=base_size/(num>0 ? num : 1);
      //--- В цикле по заголовкам ряда
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Получаем текущий и предыдущий заголовки из списка по индексу цикла
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- Если размер заголовка изменён
         if(header.Resize(w,header.Height(),false))
           {
            //--- Устанавливаем заголовку новые размеры для состояний нажат/отжат
            header.SetWidthOn(w+4);
            header.SetWidthOff(w);
            //--- Если это первый заголовок в ряду (предыдущего в списке нету),
            //--- то его смещать не нужно - идём на следующую итерацию цикла
            if(prev==NULL)
               continue;
            //--- Смещаем заголовок на координату правого края предыдущего заголовка
            if(header.Move(prev.RightEdge(),header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Растягивает заголовки вкладок по высоте элемента управления      |
//| при расположении слева                                           |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightLeft(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- В цикле по количеству рядов заголовков
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Получаем список с номером ряда, равным индексу цикла
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Получаем высоту контейнера, количество заголовков в ряду и рассчитываем высоту каждого заголовка
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- В цикле по заголовкам ряда
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Получаем текущий и предыдущий заголовки из списка по индексу цикла
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- Сохраняем изначальную высоту заголовка
         int h_prev=header.Height();
         //--- Если размер заголовка изменён
         if(header.Resize(header.Width(),h,false))
           {
            //--- Устанавливаем заголовку новые размеры для состояний нажат/отжат
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- Если это первый заголовок в ряду (предыдущего в списке нету)
            if(prev==NULL)
              {
               //--- Рассчитываем смещение по Y
               int y_shift=header.Height()-h_prev;
               //--- Смещаем заголовок на его рассчитанное смещение и идём к следующему
               if(header.Move(header.CoordX(),header.CoordY()-y_shift))
                 {
                  header.SetCoordXRelative(header.CoordX()-this.CoordX());
                  header.SetCoordYRelative(header.CoordY()-this.CoordY());
                 }
               continue;
              }
            //--- Смещаем заголовок на координату верхнего края предыдущего заголовка минус высота текущего и его рассчитанное смещение
            if(header.Move(header.CoordX(),prev.CoordY()-header.Height()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Растягивает заголовки вкладок по высоте элемента управления      |
//| при расположении справа                                          |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightRight(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- В цикле по количеству рядов заголовков
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Получаем список с номером ряда, равным индексу цикла
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Получаем высоту контейнера, количество заголовков в ряду и рассчитываем высоту каждого заголовка
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- В цикле по заголовкам ряда
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Получаем текущий и предыдущий заголовки из списка по индексу цикла
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- Если размер заголовка изменён
         if(header.Resize(header.Width(),h,false))
           {
            //--- Устанавливаем заголовку новые размеры для состояний нажат/отжат
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- Если это первый заголовок в ряду (предыдущего в списке нету),
            //--- то его смещать не нужно - идём на следующую итерацию цикла
            if(prev==NULL)
               continue;
            //--- Смещаем заголовок на координату нижнего края предыдущего заголовка
            if(header.Move(header.CoordX(),prev.BottomEdge()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

Метод, создающий указанное количество вкладок, претерпел изменения, так как нам нужно рассчитывать начальные координаты и размеры исходя из местоположения заголовков. При этом для расположения заголовков слева и справа ширине заголовка присваиваем высоту, а высоте заголовка — ширину, переданные в метод. Если заголовок расположен слева, то поворачиваем текст заголовка вертикально на 90°, а если справа — на 270:

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

//--- В цикле по количеству вкладок
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- В зависимости от расположения заголовков вкладок устанавливаем их начальные координаты
      int header_x=2;
      int header_y=0;
      int header_w=w;
      int header_h=h;
      
      //--- Устанавливаем текущую координату X или Y в зависимости от расположения заголовков вкладок
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=0;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=2;
           header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h);
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w;
           header_y=(header==NULL ? 2 : header.BottomEdgeRelative());
           break;
         default:
           break;
        }
      //--- Создаём объект TabHeader
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,255,this.Active(),false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header.SetBase(this.GetObject());
      header.SetPageNumber(i);
      header.SetGroup(this.Group()+1);
      header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
      header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
      header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
      header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
      header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
      header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
      header.SetBorderStyle(FRAME_STYLE_SIMPLE);
      header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
      header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
      header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
      header.SetAlignment(this.Alignment());
      header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      else
         this.SetHeaderText(header,"TabPage"+string(i+1));
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
         header.SetFontAngle(90);
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
         header.SetFontAngle(270);
      header.SetTabSizeMode(this.TabSizeMode());

      
      //--- Сохраняем изначальную высоту заголовка и устанавливаем его размеры в соответствии с режимом установки размеров заголовков
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Получаем смещение по Y расположения заголовка после изменения его высоты и
      //--- сдвигаем его на рассчитанную величину только для расположения заголовков слева
      int y_shift=header.Height()-h_prev;
      if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0)))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      
      //--- В зависимости от расположения заголовков вкладок устанавливаем начальные координаты полей вкладок
      int field_x=0;
      int field_y=0;
      int field_w=this.Width();
      int field_h=this.Height()-header.Height();
      int header_shift=0;
      
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           field_x=0;
           field_y=header.BottomEdgeRelative();
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         default:
           break;
        }
      
      //--- Создаём объект TabField (поле вкладки)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,255,true,false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+1);
      field.SetBorderSizeAll(1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
      field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
      field.SetForeColor(CLR_DEF_FORE_COLOR,true);
      field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
      field.Hide();
     }
//--- Выстраиваем все заголовки в соответствии с установленными режимами их отображения и выбираем указанную вкладку
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок сверху                             |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersTop(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Объявляем переменные
   int col=0;                                // Столбец
   int row=0;                                // Строка
   int x1_base=2;                            // Начальная координата X
   int x2_base=this.RightEdgeRelative()-2;   // Конечная координата X
   int x_shift=0;                            // Смещение набора вкладок для расчёта их выхода за пределы контейнера
   int n=0;                                  // Переменная для расчёта номера колонки относительно индекса цикла
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект-заголовок вкладки
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если установлен флаг расположения заголовков в несколько рядов
      if(this.Multiline())
        {
         //--- Рассчитываем значение правого края заголовка с учётом того,
         //--- что начало отсчёта всегда идёт от левого края TabControl + 2 пикселя
         int x2=header.RightEdgeRelative()-x_shift;
         //--- Если рассчитанное значение не выходит за правый край TabControl минус 2 пикселя - 
         //--- устанавливаем номер колонки равным индексу цикла минус значение в переменной n
         if(x2<x2_base)
            col=i-n;
         //--- Если рассчитанное значение выходит за правый край TabControl минус 2 пикселя
         else
           {
            //--- Увеличиваем номер ряда, рассчитываем новое смещение (чтобы очередной объект сравнивать с левый краем TabControl + 2 пикселя),
            //--- переменной n устанавливаем значение индекса цикла, и номер колонки ставим в ноль - это начало новой строки
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Назначаем заголовку вкладки номер ряда и колонки и смещаем заголовок в рассчитанные координаты
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         
        }
     }

//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков.

//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен
   if(last!=NULL)
     {
      //--- Если установлен режим растягивания заголовков на ширину контейнера - вызываем метод растягивания
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- Если это не первый ряд (с индексом 0)
      if(last.Row()>0)
        {
         //--- Рассчитываем смещение координаты Y поля вкладки
         int y_shift=last.Row()*last.Height();
         //--- В цикле по списку заголовков
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- получаем поле вкладки, соответствующее полученному заголовку
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- смещаем заголовок вкладки на рассчитанные координаты строки
            if(header.Move(header.CoordX(),header.CoordY()+y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- смещаем поле вкладки на рассчитанное смещение
            if(field.Move(field.CoordX(),field.CoordY()+y_shift))

              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- изменяем размер смещённого поля на величину его смещения
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок снизу                              |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersBottom(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Объявляем переменные
   int col=0;                                // Столбец
   int row=0;                                // Строка
   int x1_base=2;                            // Начальная координата X
   int x2_base=this.RightEdgeRelative()-2;   // Конечная координата X
   int x_shift=0;                            // Смещение набора вкладок для расчёта их выхода за пределы контейнера
   int n=0;                                  // Переменная для расчёта номера колонки относительно индекса цикла
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект-заголовок вкладки
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если установлен флаг расположения заголовков в несколько рядов
      if(this.Multiline())
        {
         //--- Рассчитываем значение правого края заголовка с учётом того,
         //--- что начало отсчёта всегда идёт от левого края TabControl + 2 пикселя
         int x2=header.RightEdgeRelative()-x_shift;
         //--- Если рассчитанное значение не выходит за правый край TabControl минус 2 пикселя - 
         //--- устанавливаем номер колонки равным индексу цикла минус значение в переменной n
         if(x2<x2_base)
            col=i-n;
         //--- Если рассчитанное значение выходит за правый край TabControl минус 2 пикселя
         else
           {
            //--- Увеличиваем номер ряда, рассчитываем новое смещение (чтобы очередной объект сравнивать с левый краем TabControl + 2 пикселя),
            //--- переменной n устанавливаем значение индекса цикла, и номер колонки ставим в ноль - это начало новой строки
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Назначаем заголовку вкладки номер ряда и колонки и смещаем заголовок в рассчитанные координаты
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()+header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         
        }
     }

//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков.

//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен
   if(last!=NULL)
     {
      //--- Если установлен режим растягивания заголовков на ширину контейнера - вызываем метод растягивания
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- Если это не первый ряд (с индексом 0)
      if(last.Row()>0)
        {
         //--- Рассчитываем смещение координаты Y поля вкладки
         int y_shift=last.Row()*last.Height();
         //--- В цикле по списку заголовков
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- получаем поле вкладки, соответствующее полученному заголовку
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- смещаем заголовок вкладки на рассчитанные координаты строки
            if(header.Move(header.CoordX(),header.CoordY()-y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- смещаем поле вкладки на рассчитанное смещение
            if(field.Move(field.CoordX(),field.CoordY()))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- изменяем размер смещённого поля на величину его смещения
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок слева                              |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersLeft(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Объявляем переменные
   int col=0;                                // Столбец
   int row=0;                                // Строка
   int y1_base=this.BottomEdgeRelative()-2;  // Начальная координата Y
   int y2_base=2;                            // Конечная координата Y
   int y_shift=0;                            // Смещение набора вкладок для расчёта их выхода за пределы контейнера
   int n=0;                                  // Переменная для расчёта номера колонки относительно индекса цикла
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект-заголовок вкладки
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если установлен флаг расположения заголовков в несколько рядов
      if(this.Multiline())
        {
         //--- Рассчитываем значение верхнего края заголовка с учётом того,
         //--- что начало отсчёта всегда идёт от нижнего края TabControl минус 2 пикселя
         int y2=header.CoordYRelative()+y_shift;
         //--- Если рассчитанное значение не выходит за верхний край TabControl минус 2 пикселя - 
         //--- устанавливаем номер колонки равным индексу цикла минус значение в переменной n
         if(y2>=y2_base)
            col=i-n;
         //--- Если рассчитанное значение выходит за верхний край TabControl минус 2 пикселя
         else
           {
            //--- Увеличиваем номер ряда, рассчитываем новое смещение (чтобы очередной объект сравнивать с левый краем TabControl + 2 пикселя),
            //--- переменной n устанавливаем значение индекса цикла, и номер колонки ставим в ноль - это начало новой строки
            row++;
            y_shift=this.BottomEdge()-header.BottomEdge()-2;
            n=i;
            col=0;
           }
         //--- Назначаем заголовку вкладки номер ряда и колонки и смещаем заголовок в рассчитанные координаты
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-header.Row()*header.Width(),header.CoordY()+y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         
        }
     }

//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков.

//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен
   if(last!=NULL)
     {
      //--- Если установлен режим растягивания заголовков на ширину контейнера - вызываем метод растягивания
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- Если это не первый ряд (с индексом 0)
      if(last.Row()>0)
        {
         //--- Рассчитываем смещение координаты X поля вкладки
         int x_shift=last.Row()*last.Width();
         //--- В цикле по списку заголовков
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- получаем поле вкладки, соответствующее полученному заголовку
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- смещаем заголовок вкладки на рассчитанные координаты строки
            if(header.Move(header.CoordX()+x_shift,header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- смещаем поле вкладки на рассчитанное смещение
            if(field.Move(field.CoordX()+x_shift,field.CoordY()))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- изменяем размер смещённого поля на величину его смещения
               field.Resize(field.Width()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Здесь заголовки располагаются слева и смещение рядов осуществляется по оси X. В остальном — логика идентична предыдущим методам.


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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок справа                             |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersRight(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Объявляем переменные
   int col=0;                                // Столбец
   int row=0;                                // Строка
   int y1_base=2;                            // Начальная координата Y
   int y2_base=this.BottomEdgeRelative()-2;  // Конечная координата Y
   int y_shift=0;                            // Смещение набора вкладок для расчёта их выхода за пределы контейнера
   int n=0;                                  // Переменная для расчёта номера колонки относительно индекса цикла
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект-заголовок вкладки
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если установлен флаг расположения заголовков в несколько рядов
      if(this.Multiline())
        {
         //--- Рассчитываем значение нижнего края заголовка с учётом того,
         //--- что начало отсчёта всегда идёт от верхнего края TabControl + 2 пикселя
         int y2=header.BottomEdgeRelative()-y_shift;
         //--- Если рассчитанное значение не выходит за нижний край TabControl минус 2 пикселя - 
         //--- устанавливаем номер колонки равным индексу цикла минус значение в переменной n
         if(y2<y2_base)
            col=i-n;
         //--- Если рассчитанное значение выходит за нижний край TabControl минус 2 пикселя
         else
           {
            //--- Увеличиваем номер ряда, рассчитываем новое смещение (чтобы очередной объект сравнивать с нижним краем TabControl минус 2 пикселя),
            //--- переменной n устанавливаем значение индекса цикла, и номер колонки ставим в ноль - это начало новой строки
            row++;
            y_shift=header.CoordYRelative()-2;
            n=i;
            col=0;
           }
         //--- Назначаем заголовку вкладки номер ряда и колонки и смещаем заголовок в рассчитанные координаты
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()+header.Row()*header.Width(),header.CoordY()-y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         
        }
     }

//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков.

//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен
   if(last!=NULL)
     {
      //--- Если установлен режим растягивания заголовков на ширину контейнера - вызываем метод растягивания
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- Если это не первый ряд (с индексом 0)
      if(last.Row()>0)
        {
         //--- Рассчитываем смещение координаты X поля вкладки
         int x_shift=last.Row()*last.Width();
         //--- В цикле по списку заголовков
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- получаем поле вкладки, соответствующее полученному заголовку
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- смещаем заголовок вкладки на рассчитанные координаты строки
            if(header.Move(header.CoordX()-x_shift,header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
               //--- изменяем размер поля вкладки на значение смещения X
               field.Resize(field.Width()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

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

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

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

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

//--- input parameters
sinput   bool                          InpMovable           =  true;                   // Panel Movable flag
sinput   ENUM_INPUT_YES_NO             InpAutoSize          =  INPUT_YES;              // Panel Autosize
sinput   ENUM_AUTO_SIZE_MODE           InpAutoSizeMode      =  AUTO_SIZE_MODE_GROW;    // Panel Autosize mode
sinput   ENUM_BORDER_STYLE             InpFrameStyle        =  BORDER_STYLE_SIMPLE;    // Label border style
sinput   ENUM_ANCHOR_POINT             InpTextAlign         =  ANCHOR_CENTER;          // Label text align
sinput   ENUM_INPUT_YES_NO             InpTextAutoSize      =  INPUT_NO;               // Label autosize
sinput   ENUM_ANCHOR_POINT             InpCheckAlign        =  ANCHOR_LEFT;            // Check flag align
sinput   ENUM_ANCHOR_POINT             InpCheckTextAlign    =  ANCHOR_LEFT;            // Check label text align
sinput   ENUM_CHEK_STATE               InpCheckState        =  CHEK_STATE_UNCHECKED;   // Check flag state
sinput   ENUM_INPUT_YES_NO             InpCheckAutoSize     =  INPUT_YES;              // CheckBox autosize
sinput   ENUM_BORDER_STYLE             InpCheckFrameStyle   =  BORDER_STYLE_NONE;      // CheckBox border style
sinput   ENUM_ANCHOR_POINT             InpButtonTextAlign   =  ANCHOR_CENTER;          // Button text align
sinput   ENUM_INPUT_YES_NO             InpButtonAutoSize    =  INPUT_YES;              // Button autosize
sinput   ENUM_AUTO_SIZE_MODE           InpButtonAutoSizeMode=  AUTO_SIZE_MODE_GROW;    // Button Autosize mode
sinput   ENUM_BORDER_STYLE             InpButtonFrameStyle  =  BORDER_STYLE_NONE;      // Button border style
sinput   bool                          InpButtonToggle      =  true ;                  // Button toggle flag
sinput   bool                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
sinput   bool                          InpListBoxMColumn    =  true;                   // ListBox MultiColumn flag
sinput   bool                          InpTabCtrlMultiline  =  true;                   // Tab Control Multiline flag
sinput   ENUM_ELEMENT_ALIGNMENT        InpHeaderAlignment   =  ELEMENT_ALIGNMENT_TOP;  // TabHeader Alignment
sinput   ENUM_ELEMENT_TAB_SIZE_MODE    InpTabPageSizeMode   =  ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode
//--- global variables


Немного увеличим (на 10 пикселей) ширину создаваемой панели:

//--- Создадим объект WinForms Panel
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",50,50,410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {

и ширину второго контейнера GroupBox — на 12 пикселей:

      //--- Создадим объект WinForms GroupBox2
      CGroupBox *gbox2=NULL;
      //--- Координатой Y GroupBox2 будет являться отступ от прикреплённых панелей на 6 пикселей
      w=gbox1.Width()+12;
      int x=gbox1.RightEdgeRelative()+1;
      int h=gbox1.BottomEdgeRelative()-6;
      //--- Если прикреплённый объект GroupBox создан
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false))
        {

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

В обработчике OnInit() после создания элемента управления TabControl, установим расположение заголовков вкладок и разрешение расположения заголовков в несколько рядов, заданные во входных параметрах советника:

            //--- Создадим объект TabControl
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false);
            //--- получим указатель на объект TabControl по его индексу в списке прикреплённых объектов с типом TabControl
            CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
            //--- Если TabControl создан и указатель на него получен
            if(tab_ctrl!=NULL)
              {
               //--- Установим расположение заголовков вкладок на элементе, текст вкладок, и создадим девять вкладок
               tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
               tab_ctrl.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
               tab_ctrl.SetMultiline(InpTabCtrlMultiline);
               tab_ctrl.SetHeaderPadding(6,0);
               tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));


При создании элемента управления ListBox в третьей вкладке элемента управления TabControl, зададим его координату Y поближе к верхней части вкладки:

               //--- Создадим объект ListBox в третьей вкладке
               int lbw=146;
               if(!InpListBoxMColumn)
                  lbw=100;
               tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,2,lbw,60,clrNONE,255,true,false);
               //--- получим указатель на объект ListBox с третьей вкладки по его индексу в списке прикреплённых объектов с типом ListBox

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

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


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


Что дальше

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

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

К содержанию

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

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

Прикрепленные файлы |
MQL5.zip (4518.96 KB)
Машинное обучение и Data Science (Часть 06): Градиентный спуск Машинное обучение и Data Science (Часть 06): Градиентный спуск
Градиентный спуск играет важную роль в обучении нейронных сетей и различных алгоритмов машинного обучения — это быстрый и умный алгоритм. Однако несмотря на его впечатляющую работу, многие специалисты по данным все еще неправильно его понимают. Давайте в этой статье посмотрим, о чем идет речь.
Разработка торговой системы на основе стандартного отклонения Разработка торговой системы на основе стандартного отклонения
Представляю вашему вниманию новую статью из серии, в которой мы учимся создавать торговые системы по показателям самых популярных технических индикаторов и пишем на их основе системы на языке MQL5 для использования в MetaTrader 5. В этой статье мы узнаем, как разработать торговую систему по индикатору стандартного отклонения.
Разработка торговой системы на основе осциллятора Чайкина Разработка торговой системы на основе осциллятора Чайкина
Это новая статья из серии, в которой мы изучаем популярные технические индикаторы и учимся создавать на их основе торговые системы. В этой статье будем работать с индикатором Chaikin Oscillator — Осциллятор Чайкина.
Технический индикатор своими руками Технический индикатор своими руками
В этой статье мы рассмотрим алгоритмы, следуя которым можно создать свой собственный технический индикатор. Мы увидим, как с помощью очень простых начальных предположений можно получить довольно сложные и интересные результаты.