Последнее обновление: 31.10.2015
TabControl
Элемент TabControl позволяет создать элемент управления с несколькими вкладками. И каждая вкладка будет хранить некоторый набор других элементов управления,
как кнопки, текстовые поля и др. Каждая вкладка представлена классом TabPage.
Чтобы настроить вкладки элемента TabControl используем свойство TabPages. При переносе элемента TabControl с панели инструментов на форму
по умолчанию создаются две вкладки — tabPage1 и tabPage2. Изменим их отображение с помощью свойства TabPages:
Нам откроется окно редактирования/добавления и удаления вкладок:
Каждая вкладка представляет своего рода панель, на которую мы можем добавить другие элементы управления,
а также заголовок, с помощью которого мы можем переключаться по вкладкам. Текст заголовка задается с помощью свойства Text.
Управление вкладками в коде
Для добавления новой вкладки нам надо ее создать и добавить в коллекцию tabControl1.TabPages с помощью метода Add:
//добавление вкладки TabPage newTabPage = new TabPage(); newTabPage.Text = "Континенты"; tabControl1.TabPages.Add(newTabPage);
Удаление так же просто:
// удаление вкладки // по индексу tabControl1.TabPages.RemoveAt(0); // по объекту tabControl1.TabPages.Remove(newTabPage);
Получая в коллекции tabControl1.TabPages нужную вкладку по индексу, мы можем ей легко манипулировать:
// изменение свойств tabControl1.TabPages[0].Text = "Первая вкладка";
SplitContainer
Элемент SplitContainer позволяет создавать две разделенные сплитером панели. Изменяя положение сплитера, можно изменить размеры этих панелей.
Используя свойство Orientation
, можно задать горизонтальное или вертикальное отображение сплитера на форму. В данном случае
это свойство принимает значения Horisontal и Vertical соответственно.
В случае, когда надо запретить изменение положения сплиттера, то можно присвоить свойству IsSplitterFixed
значение true
.
Таким образом, сплитер окажется фиксированным, и мы не сможем поменять его положение.
По умолчанию при растяжении формы или ее сужении также будет меняться размер обеих панелей сплитконтейнера. Однако мы можем закрепить за одной панелью
фиксированную ширину (при вертикальной ориентации сплиттера) или высоту (при горизонтальной ориентации сплиттера). Для этого нам надо установить у элемента
SplitContainer свойство FixedPanel
. В качестве значения оно принимает панель, которую надо зафиксировать:
Чтобы изменить положение сплитера в коде, мы можем управлять свойством SplitterDistance, которое задает положение сплиттера в пикселях
от левого или верхнего края элемента SplitContainer. А с помощью свойства SplitterIncrement
можно задать шаг, на который будет
перемещаться сплиттер при движении его с помощью клавиш-стрелок.
Чтобы скрыть одну из двух панелей, мы можем установить свойство Panel1Collapsed
или Panel2Collapsed
в
true
В один момент возникла задача сделать TabControl по отрисованному дизайну, с вкладками с левой стороны. Сложность была в том, что проект был уже написан с использованием winform, и переделывать его целиком не хотелось. Попытался реализовать эту задачу средствами класического TabControl, но встретился со множеством проблем, связанных с этим.
Первой проблемой стало то, что если спозиционировать вкладки с левой стороны, то мы получаем следующую картину:
Но мне нужно было, чтобы надписи шли гаризонтально. Изучив чуть глубже данный контрол, решил воспользоваться параметром DrawMode=OwnerDrawFixed. Все надписи стерлись, и на кнопке стало возможным писать и рисовать. Но остался фон самой кнопки, который полностью закрасить не удалось.
Следующим шагом поменял Appearance c Normal на Buttons, был еще вариант FlatButtons, но через конструктор его поставить не удалось, а выставление в коде ни на что не повлияло.
В режиме Buttons вылезла такая ерунда:
На картинке видно, что между кнопками и набором TabPages появилось расстояние. Отуда оно взялось и каким параметром регулируется, мне выяснить так и не удалось.
Еще некоторое время я изучал существующие платные и бесплатные библиотеки контролов на наличие возможности изменения под себя вкладок TabControl, но они либо предлагали использовать заранее созданные стили, либо позволяли максимум поменять цвет.
В итоге намучившись с ним, я решил написать свой контрол, взяв за основу стандартный. Целью стало скрыть стандартные вкладки и на смену им поставить свои, завязав их на контрол.
Постараюсь подробно описать все, что для этого пришлось сделать.
Шаг 1
Для начала в проекте нужно создать новый котрол. Для этого в панели Solution Explorer кликаем правой кнопкой по проекту, далее Add->Component, в открывшейся панели вводим имя нового контрола (у меня это NewTabControl)
Шаг 2
После создания открываем код созданного контрола. В коде делаем следующие правки:
дописываем
using System.Windows.Forms;
using System.Drawing;
Создаем три класса, наследуя их от классов стандартных контролов.
Класс нового TabControl
public partial class NewTabControl: System.Windows.Forms.TabControl
Класс нового контрола
public class NewTabPanel: System.Windows.Forms.Panel
Класс одной вкладки
public class PanelTP: System.Windows.Forms.Panel
Теперь нам нужно перезагрузить следующий метод в классе NewTabControl:
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x1328 && !DesignMode) m.Result = (IntPtr)1;
else base.WndProc(ref m);
}
Данное действие позволит нам скрыть стандартные вкладки.
Теперь нам нужно связать все классы между собой. Не буду описывать весь код, его я приложу к данной статье. Опишу только самые интересные моменты.
Шаг 3
Компонуем все элементы в классе NewTabPanel:
private void InitializeComponent()
{
this.panel2 = new System.Windows.Forms.Panel(); //Панель с вкладками
this.tabControl = new NewTabControl();
this.Controls.Add(this.tabControl);
this.Controls.Add(this.panel2);
this.Size = new System.Drawing.Size(311, 361);
this.panel2.Dock = System.Windows.Forms.DockStyle.Left;
this.tabControl.Dock = System.Windows.Forms.DockStyle.Fill;
tabControl.ControlAdded += new ControlEventHandler(tc_ControlAdded); //Событие на создание новой вкладки
tabControl.ControlRemoved += new ControlEventHandler(tc_ControlRemoved); //Событие удаления вкладки
tabControl.Selected += new TabControlEventHandler(tc_Selected); //Событие выделения вкладки
}
Шаг 4
Теперь можно задать формат, как будет выглядеть сама вкладка.
На данном этапе вы можете сами расположить текст, картинку или любой другой элемент на будущей вкладке. А также задать размер и фон самой вкладки.
У себя я вывожу иконку и название вкладки.
В классе PanelTP создаем метод:
private void InitializeComponent()
{
this.Height = 27;
this.Width = 128;
this.BackgroundImage = Tabpanel.Properties.Resources.tab_c_74;
this.Click += new EventHandler(Select_Item);
PictureBox Icon;
Icon = new PictureBox();
Icon.Width = 25;
Icon.Height = 26;
Icon.Left = 3;
Icon.Top = 5;
Icon.Image = Tabpanel.Properties.Resources.green_dialbut_611;
this.Controls.Add(Icon);
Label lname;
lname = new Label();
lname.Width = 95;
lname.Height = 25;
lname.Left = 28;
lname.Top = 5;
lname.Font = new System.Drawing.Font("Times New Roman", 8f, FontStyle.Regular);
lname.Text = this.name;
lname.Click += new EventHandler(Select_Item);
this.Controls.Add(lname);
}
Шаг 5
Не буду описывать методы, обрабатывающие события, они подробно описаны в приложенном проекте. Перейду к применению.
После того как мы все сохранили, на панели Toolbox появятся новые компоненты
Теперь мы можем его разместить в нашей форме как захотим.
Чтобы добавить вкладку используем:
newTabPanel1.TabPages.Add("TabName");
Чтобы удалить:
newTabPanel1.TabPages.Remove(newTabPanel1.TabPages[id])
Где id — это номер вкадки
При такой реализации TabControl вы всегда сможете настроить сортировку между вкладками или скрыть выбранную вкладку, сделать вкладку любого размера и оформления, сделать панель с вкладками соврачиваемой и разворачиваемой.
По такому же принципу вы можете создать любой свой контрол скомпоновав и запрограммировав его из имеющихся.
Возможно, для кого-то я описал очевидные вещи, но надеюсь, найдутся и те, кому данная статья будет полезна.
Исходники проекта можно скачать тут.
Бинарник тут.
Возможно вам также будет интересна моя статья Как подключить сторонний браузер в приложении на C#
Windows TabControl is a useful control that allows you display multiple dialogs tabs on a single form by switching between the tabs. A tab acts as another Form that can host other controls. Figure 1 shows an example of TabControl in Visual Studio .NET, which allows you to switch among multiple files using the tabs.
In this tutorial, I will explain how to create and use a TabControl in your Windows applications with C#.
Adding a TabControl to Form
The simplest way to create a TabControl is using Visual Studio .NET. I create a Windows Form application using Visual Studio .NET and add a TabControl from Toolbox to the Form by dragging the TabControl to the Form. After that I resize and reposition TabControl according to the Form size. The Form Designer adds the code for TabControl for you. If you see the code, you will notice once private variable of type System.Windows.Forms.TabControl as following:
- private System.Windows.Forms.TabControl tabControl1;
The System.Windows.Forms.TabControl class represents a TabControl in .NET. Now if you see the InitializeComponent method generated by the Form Designer, you will see the code for TabControl such as setting TabControl location, name, size and adding the TabControl to the Form controls. See Listing 1.
- private void InitializeComponent()
- {
- this.tabControl1 = new System.Windows.Forms.TabControl();
- this.SuspendLayout();
- this.tabControl1.Location = new System.Drawing.Point(8, 16);
- this.tabControl1.Name = «tabControl1»;
- this.tabControl1.SelectedIndex = 0;
- this.tabControl1.Size = new System.Drawing.Size(352, 248);
- this.tabControl1.TabIndex = 0;
- this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
- this.ClientSize = new System.Drawing.Size(368, 273);
- this.Controls.Add(this.tabControl1);
- this.Name = «Form1»;
- this.Text = «Form1»;
- this.ResumeLayout(false);
- }
Figure 1 shows an example
Understanding the TabControl and TabPage class
A TabControl is a collection of tab pages and a tab page is the actual control that hosts other child controls. TabPage class represents a tab page.
TabControl class represents a TabControl. This class provides members (properties, methods, and events) to work with the TabControls. Table 1 lists the TabControl properties.
Property | Description |
Alignment | Area of the control where the tabs are aligned. |
Appearance | Visual appearance of the control’s tabs. |
DrawMode | A way that the control’s tab pages are drawn. |
HotTrack | Value indicating whether the control’s tabs change in appearance when the mouse passes over them. |
ImageList | The images to display on the control’s tabs. |
ItemSize | Size of the control’s tabs. |
Multiline | A value indicating whether more than one row of tabs can be displayed. |
Padding | Amount of space around each item on the control’s tab pages. |
RowCount | Returns the number of rows that are currently being displayed in the control’s tab strip. |
SelectedIndex | The index of the currently-selected tab page. |
SelectedTab | Currently selected tab page. |
ShowToolTips | The value indicating whether a tab’s ToolTip is shown when the mouse passes over the tab. |
SizeMode | The way that the control’s tabs are sized. |
TabCount | Number of tabs in the tab strip. |
TabPages | Returns the collection of tab pages in this tab control. |
Adding TabPage to a TabControl
Now I will add few tabs to the TabControl with the help of Properties window of TabControl. The Properties window has a property called TabPages, which is a collection of TabPage controls (see Figure 2). A TabPage represents a page of the TabControl that can host child controls.
Figure 2. TabPages property of TabControl
Now if you click on TabPages property in Property window, it launches TabPage Collection Editor (see Figure 3) where you can add a new page or remove existing pages by using Add and Remove buttons. You can also set the properties of pages by using the right side properties grid. As you can see from Figure 3, I add two pages and set their properties.
Figure 3. Adding Tab pages to a TabControl
After adding two pages to TabControl, the final Form looks like Figure 4.
Figure 4. A Form with two Tab pages
Adding and Removing a TabPage to TabControl Programmatically
You can add and remove Tab pages to a TabControl using the TabControl.TabPages.Add and TabControl.TabPages.Remove methods. The following code snippet adds a new page to the TabControl programmatically:
- TabPage newPage = new TabPage(«New Page»);
- tabControl1.TabPages.Add(newPage);
After adding the page, the new TabControl would look like Figure 5.
Figure 5. Adding a Tab page programmatically.
The Remove method of TabPageCollection class (through TabControl.TabPages) removes a page by name or index from the page collection. The following code snippet removes «New Page» from the collection:
- TabPage newPage = new TabPage(«New Page»);
- tabControl1.TabPages.Remove(newPage);
The RemoveAll method removes all the pages from the collection.
Adding Controls to a TabPage
Adding controls to a TabPage is similar to adding controls to a Form. Make a page active in the Form Designer and drag and drop controls from Toolbox to the page. I add a Label, a TextBox, and a Button control to Settings page of TabControl and change their properties. The final page looks like Figure 6.
Figure 6. Adding controls to a Tab page
Controls are added to a page by using TabPage.Controls.Add method. Now if you see the code generated by the designer, you will notice the following code:
- this.SettingsPage.Controls.Add(this.BrowseBtn);
- this.SettingsPage.Controls.Add(this.textBox1);
- this.SettingsPage.Controls.Add(this.label1);
Using the same code, you can even add controls to a TabPage programmatically.
Access Controls of a TabPage
All controls of a TabPage are local to a Form and accessible from the Form without adding any additional functionality. For example, the following code sets the Text property of the TextBox on Preferences Tab page:
- this.textBox1 =@«C:\»;
Getting and Setting Active Tab Programmatically
You can get and set an active tab of a TabControl programmatically using the SelectedTab property of TabControl. For example, the following code snippet sets PreferencePage as active tab:
- this.tabControl1.SelectedTab = this.PreferencesPage;
Содержание
- Концепция
- Доработка классов библиотеки
- Объект TabField — поле вкладки элемента управления TabControl
- Тестирование
- Что дальше
Концепция
Продолжаем тему WinForms-объекта TabControl. Если у элемента управления вкладок больше, чем их может разместиться по ширине объекта (имеем в виду их расположение сверху), то те заголовки, которые не умещаются в пределах элемента, могут либо быть обрезанными по краю с наличием кнопок их прокрутки, либо, если у объекта установлен флаг режима Multiline, то заголовки размещаются по несколько штук (сколько входит в размер элемента) в несколько рядов. Для режима их расположения в несколько рядов есть три способа задания размера вкладок (SizeMode):
- Normal — ширина вкладок устанавливается по ширине текста заголовка, по краям заголовка добавляется пространство, указанное в значениях PaddingWidth и PaddingHeight заголовка;
- Fixed — фиксированный размер, указываемый в настройках элемента управления. Текст заголовков обрезается, если не входит в его размеры;
- FillToRight — вкладки, умещающиеся в пределах ширины элемента управления, растягиваются на всю его ширину.
При выборе вкладки в активном режиме Multiline, её заголовок, которой не граничит с полем вкладки, вместе со всей строкой, в которой он находится, перемещается вплотную к полю вкладки, а те заголовки, которые были примкнуты к полю, встают на место строки выбранной вкладки.
Сегодня реализуем такой режим. Но сделаем его только для расположения вкладок сверху элемента управления, и для режима размера вкладки Normal и Fixed. Режим FillToRight и расположение вкладок снизу, слева и справа во всех трёх режимах размера вкладок будем реализовывать в последующих статьях, равно как и режим прокрутки вкладок, расположенных в одной строке при отключенном режиме Multiline.
Для взаимодействия с полем вкладки, ранее реализованным как объект-контейнер из класса CContainer, создадим новый объект TabField — наследник объекта-контейнера со своими свойствами и методами для полноценной работы с полем вкладки.
Доработка классов библиотеки
В файле библиотеки \MQL5\Include\DoEasy\Defines.mqh в список типов графических элементов добавим новый тип вспомогательного WinForms-объекта:
enum ENUM_GRAPH_ELEMENT_TYPE { GRAPH_ELEMENT_TYPE_STANDARD, GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED, GRAPH_ELEMENT_TYPE_SHADOW_OBJ, GRAPH_ELEMENT_TYPE_ELEMENT, GRAPH_ELEMENT_TYPE_FORM, GRAPH_ELEMENT_TYPE_WINDOW, GRAPH_ELEMENT_TYPE_WF_UNDERLAY, GRAPH_ELEMENT_TYPE_WF_BASE, GRAPH_ELEMENT_TYPE_WF_CONTAINER, GRAPH_ELEMENT_TYPE_WF_PANEL, GRAPH_ELEMENT_TYPE_WF_GROUPBOX, GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL, GRAPH_ELEMENT_TYPE_WF_COMMON_BASE, GRAPH_ELEMENT_TYPE_WF_LABEL, GRAPH_ELEMENT_TYPE_WF_BUTTON, GRAPH_ELEMENT_TYPE_WF_CHECKBOX, GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON, GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX, GRAPH_ELEMENT_TYPE_WF_LIST_BOX, GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX, GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX, GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM, GRAPH_ELEMENT_TYPE_WF_TAB_HEADER, GRAPH_ELEMENT_TYPE_WF_TAB_FIELD, };
Так как этот объект как самостоятельная единица работать не будет, то он является вспомогательным, и работает в составе элемента управления TabControl, наряду с таким же вспомогательным объектом TabHeader, созданным нами в прошлой статье.
Добавим новое перечисление режимов установки размеров вкладок:
enum ENUM_CANV_ELEMENT_ALIGNMENT { CANV_ELEMENT_ALIGNMENT_TOP, CANV_ELEMENT_ALIGNMENT_BOTTOM, CANV_ELEMENT_ALIGNMENT_LEFT, CANV_ELEMENT_ALIGNMENT_RIGHT, }; enum ENUM_CANV_ELEMENT_TAB_SIZE_MODE { CANV_ELEMENT_TAB_SIZE_MODE_NORMAL, CANV_ELEMENT_TAB_SIZE_MODE_FIXED, CANV_ELEMENT_TAB_SIZE_MODE_FILL, };
В конце списка целочисленных свойств графического элемента на канвасе впишем два новых свойства и увеличим значение общего количества целочисленных свойств с 88 до 90:
enum ENUM_CANV_ELEMENT_PROP_INTEGER { CANV_ELEMENT_PROP_ID = 0, CANV_ELEMENT_PROP_TYPE, CANV_ELEMENT_PROP_TAB_MULTILINE, CANV_ELEMENT_PROP_TAB_ALIGNMENT, CANV_ELEMENT_PROP_TAB_SIZE_MODE, CANV_ELEMENT_PROP_TAB_PAGE_NUMBER, CANV_ELEMENT_PROP_ALIGNMENT, }; #define CANV_ELEMENT_PROP_INTEGER_TOTAL (90) #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_MULTILINE, SORT_BY_CANV_ELEMENT_TAB_ALIGNMENT, SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE, SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER, SORT_BY_CANV_ELEMENT_ALIGNMENT, SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP, SORT_BY_CANV_ELEMENT_NAME_RES, SORT_BY_CANV_ELEMENT_TEXT, SORT_BY_CANV_ELEMENT_DESCRIPTION, };
Теперь мы сможем сортировать, фильтровать, выбирать и искать объекты по этим новым свойствам.
Так как новых свойств у нас будет далее добавляться всё больше, но не для каждого графического объекта они предназначены, то хочу пояснить, что пока все вновь добавляемые свойства доступны во всех объектах. Но далее мы ограничим их доступность для объектов, где таких свойств быть не должно, простым добавлением в классы этих объектов методов, возвращающих флаг поддержания того, или иного свойства. Такие методы — виртуальные, давно у нас есть в базовых классах библиотеки для каждого объекта. Здесь мы их не добавляем для каждого нового объекта по одной простой причине: когда будем считать, что все объекты созданы и стоит навести порядок в доступности свойств — вот тогда всё разом и сделаем. И делать будем так, чтобы все свойства были видны наглядно для каждого из объектов-элементов управления — на панели их свойств, созданной на графике.
Новые свойства и перечисления добавили. Теперь добавим тексты для вывода их описания.
В файле \MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений библиотеки:
MSG_LIB_TEXT_TOP,
MSG_LIB_TEXT_BOTTOM,
MSG_LIB_TEXT_LEFT,
MSG_LIB_TEXT_RIGHT,
MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL,
MSG_LIB_TEXT_TAB_SIZE_MODE_FILL,
MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED,
MSG_LIB_TEXT_CORNER_LEFT_UPPER,
MSG_LIB_TEXT_CORNER_LEFT_LOWER,
MSG_LIB_TEXT_CORNER_RIGHT_LOWER,
MSG_LIB_TEXT_CORNER_RIGHT_UPPER,
…
MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,
MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,
MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,
MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,
MSG_GRAPH_OBJ_BELONG_PROGRAM,
…
MSG_BUTT_LIST_ERR_FAILED_SET_GROUP_BUTTON,
MSG_BUTT_LIST_ERR_FAILED_SET_TOGGLE_BUTTON,
MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ,
…
MSG_CANV_ELEMENT_PROP_TAB_MULTILINE,
MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT,
MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE,
MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,
MSG_CANV_ELEMENT_PROP_ALIGNMENT,
и текстовые сообщения, соответствующие вновь добавленным индексам:
{"Сверху","Top"}, {"Снизу","Bottom"}, {"Слева","Left"}, {"Справа","Right"}, {"По ширине текста заголовка вкладки","Fit to tab title text width"}, {"По ширине элемента управления TabControl","Fit TabControl Width"}, {"Фиксированный размер","Fixed size"}, {"Центр координат в левом верхнем углу графика","Center of coordinates is in the upper left corner of the chart"}, {"Центр координат в левом нижнем углу графика","Center of coordinates is in the lower left corner of the chart"}, {"Центр координат в правом нижнем углу графика","Center of coordinates is in the lower right corner of the chart"}, {"Центр координат в правом верхнем углу графика","Center of coordinates is in the upper right corner of the chart"},
…
{"Элемент управления \"ButtonListBox\"","Control element \"ButtonListBox\""}, {"Заголовок вкладки","Tab header"}, {"Поле вкладки","Tab field"}, {"Элемент управления \"TabControl\"","Control element \"TabControl\""}, {"Графический объект принадлежит программе","The graphic object belongs to the program"},
…
{"Не удалось установить группу кнопке с индексом ","Failed to set group for button with index "}, {"Не удалось установить флаг \"Переключатель\" кнопке с индексом ","Failed to set the \"Toggle\" flag on the button with index "}, {"Не удалось получить вкладку элемента управления TabControl","Failed to get tab of TabControl"},
…
{"Несколько рядов вкладок в элементе управления","Multiple rows of tabs in a control"}, {"Местоположение вкладок внутри элемента управления","Location of tabs inside the control"}, {"Режим установки размера вкладок","Tab Size Mode"}, {"Порядковый номер вкладки","Tab ordinal number"}, {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
Так как у нас появился новый режим рисования заголовков вкладок, то нам нужно сделать возврат описания выбранного режима. В файле сервисных функций библиотеки \MQL5\Include\DoEasy\Services\DELib.mqh напишем функцию, возвращающую описание режима установки размера вкладок:
string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { switch(mode) { case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL); break; case CANV_ELEMENT_TAB_SIZE_MODE_FIXED : return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED); break; case CANV_ELEMENT_TAB_SIZE_MODE_FILL : return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FILL); break; default : return "Unknown"; break; } }
В функцию передаётся режим установки размера вкладок и, в зависимости от него, возвращается соответствующее текстовое сообщение.
В файле класса базового графического объекта библиотеки \MQL5\Include\DoEasy\Objects\Graph\GBaseObj.mqh, в его методе, возвращающем описание типа графического элемента, создадим раздел для вспомогательных объектов, переместим туда возврат описания заголовка вкладки, и добавим возврат описания нового типа — поля вкладки:
string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type) { return ( type==GRAPH_ELEMENT_TYPE_STANDARD ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD) : type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) : type==GRAPH_ELEMENT_TYPE_ELEMENT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT) : type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ) : type==GRAPH_ELEMENT_TYPE_FORM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM) : type==GRAPH_ELEMENT_TYPE_WINDOW ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW) : type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY) : type==GRAPH_ELEMENT_TYPE_WF_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE) : type==GRAPH_ELEMENT_TYPE_WF_CONTAINER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER) : type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX) : type==GRAPH_ELEMENT_TYPE_WF_PANEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL) : type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL) : type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE) : type==GRAPH_ELEMENT_TYPE_WF_LABEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL) : type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX) : type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON) : type==GRAPH_ELEMENT_TYPE_WF_BUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON) : type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM) : type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER) : type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD) : "Unknown" ); }
В прошлой статье, в самом конце, обсуждали возможность немного оптимизировать метод, ищущий количество объектов определённого типа для создания его имени. Там мы заметили, что в двух методах, работающих совместно, два раза вызывается метод, создающий имя объекта из строкового представления константы перечисления, указывающей на тип объекта.
Чтобы избежать двойного вызова метода, в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, где расположены методы для создания имён графических элементов, в защищённой секции класса, добавим ещё один — перегруженный метод, в который будем передавать уже ранее созданное, и теперь известное имя объекта для его поиска среди других графических объектов на графике:
int GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const; int GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const; string CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type); private:
За пределами тела класса напишем его реализацию:
int CGCnvElement::GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const { int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow()); string name=TypeGraphElementAsString(type); for(int i=0;i<total;i++) { string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow()); if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE) continue; if(::StringFind(name_obj,name)>0) n++; } return n; } int CGCnvElement::GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const { int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow()); for(int i=0;i<total;i++) { string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow()); if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE) continue; if(::StringFind(name_obj,name)>0) n++; } return n; }
По сравнению с его «парным» методом, в котором имя объекта создаётся из его типа, а затем ищется в имени графического объекта, здесь мы это имя получаем во входных параметрах метода и ищем в имени объекта подстроку, содержащую переданное в метод имя.
В общем ничего сложного, но один вызов метода создания имени мы таким образом убрали. Там, где имя ещё не известно — используем вызов первого метода, а где известно — второго.
Ранее метод, создающий и возвращающий имя графического элемента по его типу, приводил к двойному вызову метода создания имени объекта из его типа, первый раз внутри метода, а второй — внутри вызываемого метода, в котором тоже вызывался метод создания имени объекта из его типа:
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type) { return this.NamePrefix()+TypeGraphElementAsString(type)+(string)this.GetNumGraphElements(type); }
Сейчас же в этом методе сделаем изменения: создадим имя объекта, используем его для построения строки возврата и отправим его в новый перегруженный метод поиска количества таких объектов:
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type) { string name=TypeGraphElementAsString(type); return this.NamePrefix()+name+(string)this.GetNumGraphElements(name,type); }
В методе, создающем графический объект-элемент, была немного раздражающая недоработка — при ошибке создания графического объекта, метод всегда возвращал отсутствие ошибки (код 0), но объект не создавался. Лишь по косвенным признакам можно было догадываться о причинах ошибки. Это скорее относится к разработке классов графических элементов, чем к их использованию пользователем библиотеки, так как все ошибки создания объектов уже исключены на стадии разработки классов. Но всё же внесём изменения, позволяющие более точно понять причину ошибки:
bool CGCnvElement::Create(const long chart_id, const int wnd_num, const int x, const int y, const int w, const int h, const bool redraw=false) { ::ResetLastError(); if(this.m_canvas.CreateBitmapLabel((chart_id==NULL ? ::ChartID() : chart_id),wnd_num,this.m_name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE)) { this.Erase(CLR_CANV_NULL); this.m_canvas.Update(redraw); this.m_shift_y=(int)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_WINDOW_YDISTANCE,wnd_num); return true; } int err=::GetLastError(); int code=(err==0 ? (w<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH : h<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT : ERR_OBJECT_ERROR) : err); string subj=(w<1 ? "Width="+(string)w+". " : h<1 ? "Height="+(string)h+". " : ""); CMessage::ToLog(DFUN_ERR_LINE+subj,code,true); return false; }
В переменную считаем код последней ошибки (и вот он-то и был всегда нулевым при ошибке создания графического ресурса в классе CCanvas Стандартной Библиотеки), далее проверяем код ошибки, и если он равен нулю, то проверяем ширину и высоту создаваемого объекта. Если какая-либо из этих величин меньше единицы, то записываем в код ошибки код соответствующего сообщения, либо общую ошибку создания графического объекта. Если же код ошибки не нулевой — записываем в переменную код ошибки. Затем создаём строку с дополнительным описанием кода ошибки — опять-таки, в зависимости от переданных в метод величин ширины и высоты, и выводим сообщение с указанием имени метода с номером строки, дополнительного сообщения и описания кода ошибки.
Объекты графических элементов все являются наследниками класса объекта-формы, который в свою очередь тоже является наследником других классов. Но в нём создан функционал для работы с мышкой, поэтому все объекты графического интерфейса программы так или иначе основаны на нём. И каждый объект, который прикреплён к своему базовому объекту, т.е., который создаётся из базового, наследует свойства своего создателя. К этим свойствам также относятся и такие свойства, как активность, видимость и доступность. Если объект, из которого создан прикреплённый к нему другой объект, не активен, т.е. не реагирует на курсор мышки, то и его подчинённый объект должен наследовать это поведение (которое позже можно изменить). Если объект не доступен (не активен, и окрашен серым цветом, указывая на свою неактивность), то и подчинённый тоже должен быть таким же. А если объект невидим, то скрытым должен быть и подчинённый, что тоже естественно.
В файле \MQL5\Include\DoEasy\Objects\Graph\Form.mqh класса объекта-панели, в его методе создания нового присоединённого элемента и добавления его в список присоединённых объектов, пропишем для всех подчинённых объектов (объектов, создаваемых из своих родителей) наследование этих свойств:
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity) { if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19)); return NULL; } int num=this.m_list_elements.Total(); string descript=TypeGraphElementAsString(element_type); int elm_x=x; int elm_y=y; this.GetCoords(elm_x,elm_y); CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity); if(obj==NULL) return NULL; if(!this.AddNewElement(obj,elm_x,elm_y)) { delete obj; return NULL; } obj.SetBackgroundColor(colour,true); obj.SetOpacity(opacity); obj.SetActive(activity); obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); obj.SetID(this.GetMaxIDAll()+1); obj.SetNumber(num); obj.SetCoordXRelative(obj.CoordX()-this.CoordX()); obj.SetCoordYRelative(obj.CoordY()-this.CoordY()); obj.SetZorder(this.Zorder(),false); obj.SetCoordXRelativeInit(obj.CoordXRelative()); obj.SetCoordYRelativeInit(obj.CoordYRelative()); obj.SetVisible(this.IsVisible(),false); obj.SetActive(this.Active()); obj.SetEnabled(this.Enabled()); return obj; }
Теперь каждый создаваемый новый прикреплённый объект будет сразу наследовать эти свойства от своего базового объекта, и у нас не получится так, что на неактивном объекте вдруг какой-то созданный его подчинённый начнёт проявлять активность, либо из скрытого объекта вдруг на графике появится его подчинённый объект, и т.п.
Таким же образом и обработчики событий мышки должны пропускать неактивные или скрытые объекты.
В этом же файле, в обработчик последнего события мышки, впишем строки, запрещающие обработку для скрытого или недоступного объекта:
void CForm::OnMouseEventPostProcessing(void) { if(!this.IsVisible() || !this.Enabled()) return; ENUM_MOUSE_FORM_STATE state=this.GetMouseState(); switch(state) {
Если объект является таковым — просто выходим из метода.
У нас сделано так, что если у объекта есть рамка, то она рисуется только в том случае, если для объекта в методе его очистки и заполнения цветом фона установлен флаг его перерисовки. Это неправильно. Не всегда нужно рисовать объекты с обязательной перерисовкой всего графика. Но в этом случае — если флаг перерисовки снят, то у объекта пропадает рамка при вызове этого метода. Тем более, что для рисования рамки уже есть условие — её тип задан как не отсутствующий. Поэтому во всех классах, где есть методы Erase(), удалим проверку флага перерисовки для отображения рамки объекта:
if(this.BorderStyle()!=FRAME_STYLE_NONE && redraw)
В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh напишем пустой виртуальный метод для рисования рамки объекта:
class CWinFormBase : public CForm { protected: color m_fore_color_init; color m_fore_state_on_color_init; private: uint GetFontFlags(void); public: virtual void DrawFrame(void){} CArrayObj *GetListElementsByType(const ENUM_GRAPH_ELEMENT_TYPE type); int ElementsTotalByType(const ENUM_GRAPH_ELEMENT_TYPE type); CGCnvElement *GetElementByType(const ENUM_GRAPH_ELEMENT_TYPE type,const int index);
Метод нужен, чтобы можно было в классах-наследниках, где требуется рисование рамки, переопределить и использовать именно этот метод, а не ранее написанные и используемые методы DrawFrameBevel(), DrawFrameFlat(), DrawFrameSimple() и DrawFrameStamp() класса объекта-формы — всё же эти методы предназначены для другого — для рисования именно определённой рамки объекта-формы. Если же для какого-либо графического элемента нужно нарисовать свою уникальную рамку, то нужно в том классе переопределить объявленный здесь метод и нарисовать с его помощью нужную рамку.
В методах Erase() теперь удалена проверка флагов обновления для рисования рамки:
void CWinFormBase::Erase(const color colour,const uchar opacity,const bool redraw=false) { CGCnvElement::Erase(colour,opacity,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle()); this.Update(redraw); } void CWinFormBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false) { CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle()); this.Update(redraw); }
Теперь рамка рисуется всегда, если для неё задан её тип. Так сделано во всех файлах всех классов, в которых есть методы Erase.
В конец метода, возвращающего описание целочисленного свойства элемента, добавим блоки кода для возврата новых свойств графических элементов:
property==CANV_ELEMENT_PROP_TAB_ALIGNMENT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property)) ) : 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_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)) ) : "" ); }
Теперь любой графический элемент сможет вернуть строку с описанием указанного нового свойства и его значения.
В некоторых защищённых конструкторах графических элементов, в самом конце кода конструктора, у нас сейчас прописана строка, вызывающая перерисовку созданного объекта:
this.Redraw(false);
Это неправильно. Объект должен перерисовываться только после его окончательного создания, а не на каждом очередном конструкторе всей иерархии наследования создаваемого объекта.
Вот если представить цепочку иерархии объекта: Obj0 —> Obj1 —> Obj2 —> Obj3 —> Obj4 —> … … —> ObjN, где Obj0 — это самый первый объект в иерархии, а ObjN — самый последний, который и создаётся, то при его создании будут поочерёдно вызваны все конструкторы всей цепочки наследования. И если в каждом из них будет стоять указанная строка с обновлением, то каждый раз объект будет перерисован.
Уберём эти строки из всех защищённых конструкторов всех классов.
Как пример, в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CommonBase.mqh:
CCommonBase::CCommonBase(const ENUM_GRAPH_ELEMENT_TYPE type, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CWinFormBase(type,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(type); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetCoordX(x); this.SetCoordY(y); this.SetWidth(w); this.SetHeight(h); this.Initialize(); if(this.AutoSize()) this.AutoSetWH(); this.SetWidthInit(this.Width()); this.SetHeightInit(this.Height()); this.SetCoordXInit(x); this.SetCoordYInit(y); this.Redraw(false); }
Такие же изменения уже сделаны в классах:
CLabel в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Label.mqh и CButton в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh.
Здесь же, в этом же файле класса CCommonBase сделаем вышеозвученные изменения в методах Erase() по удалению проверки флага перерисовки:
void CCommonBase::Erase(const color colour,const uchar opacity,const bool redraw=false) { CGCnvElement::Erase(colour,opacity,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle()); this.Update(redraw); } void CCommonBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false) { CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle()); this.Update(redraw); }
Далее в других файлах других классов мы эти изменения описывать не будем.
Так как теперь для отрисовки рамки объекта не нужно принудительно отправлять в метод установленный флаг перерисовки, то в файле класса объекта-кнопки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, в методе перерисовки объекта укажем не принудительно true, как это было раньше и что вызывало перерисовку всего графика, а передадим флаг redraw, в свою очередь передаваемый в метод извне и от которого будет зависеть необходимость перерисовки:
void CButton::Redraw(bool redraw) { this.Erase(this.BackgroundColor(),this.Opacity(),redraw); int x=0,y=0; CLabel::SetTextParamsByAlign(x,y); this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor()); this.Update(redraw); }
В методе, устанавливающем состояние кнопки как «отжато» для всех Button одной группы в контейнере, в самом конце добавим перерисовку графика, на котором создан объект-кнопка, для немедленного отображения изменений после завершения обработки всех кнопок:
void CButton::UnpressOtherAll(void) { CWinFormBase *base=this.GetBase(); if(base==NULL) return; CArrayObj *list=base.GetListElementsByType(this.TypeGraphElement()); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL); if(list!=NULL) { for(int i=0;i<list.Total();i++) { CButton *obj=list.At(i); if(obj==NULL) continue; obj.SetState(false); obj.SetBackgroundColor(obj.BackgroundColorInit(),false); obj.SetForeColor(obj.ForeColorInit(),false); obj.SetBorderColor(obj.BorderColorInit(),false); obj.Redraw(false); } } ::ChartRedraw(this.ChartID()); }
В обработчике последнего события мышки, в самом его начале, добавим проверку невидимости или недоступности элемента, как это уже делали выше для объекта-формы:
void CButton::OnMouseEventPostProcessing(void) { if(!this.IsVisible() || !this.Enabled()) return; ENUM_MOUSE_FORM_STATE state=GetMouseState(); switch(state) {
Такие же доработки уже сделаны в классах:
CCheckBox в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh и CTabHeader в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.
Далее такие изменения уже рассматривать не будем.
Объект TabField — поле вкладки элемента управления TabControl
Для элемента управления TabControl мы в прошлой статье создали класс вспомогательного объекта-заголовка вкладки TabHeader. Класс унаследован от объекта-кнопки, так как повторяет практически весь её функционал. Такой заголовок напрямую связан с полем вкладки, которые в совокупности составляют одну вкладку. А сам элемент управления состоит как минимум из двух таких вкладок.
В прошлой статье мы для реализации поля вкладки использовали объект-контейнер. Это базовый объект для всех объектов-контейнеров в библиотеке. На поле вкладки должны располагаться подчинённые объекты, создаваемые на этом поле, и, соответственно, подчинённые ему. Функционала базового объекта-контейнера для реализации работы с полем, естественно, недостаточно. Поэтому на основе базового объекта-контейнера создадим новый класс объекта-поля вкладки.
В папке библиотеки \MQL5\Include\DoEasy\Objects\Graph\WForms\ создадим новый файл TabField.mqh класса CTabField. Класс должен быть унаследован от базового класса объекта-контейнера. Файл объекта-панели должен быть подключен к файлу создаваемого класса, что даст к нему доступ всем файлам графических объектов библиотеки:
#property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/ru/users/artmedia70" #property version "1.00" #property strict #include "Containers\Panel.mqh" class CTabField : public CContainer { }
В приватной секции объявим метод, возвращающий указатель на соответствующий этому полю объект-заголовок, и виртуальный метод для создания прикреплённых ко вкладке (к этому объекту-полю) графических элементов:
class CTabField : public CContainer { private: CWinFormBase *GetHeaderObj(void); virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string descript, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); protected:
Так как если в этом файле указать тип объекта-заголовка именно его типом (CTabHeader), который виден в этом классе, и попытаться скомпилировать всю библиотеку компиляцией главного класса библиотеки CEngine, то получим большое количество ошибок и предупреждений о неизвестности типа CTabHeader. Голову ломать не будем в поисках, где там что «застряло» во всех хитросплетениях всех файлов библиотеки, а просто объявим возвращаемый тип как базовый объект всех WinForms-объектов библиотеки. Этого будет достаточно для работы с ним здесь. А за пределами этого класса мы его уже можем получить отсюда с его правильным типом.
Виртуальный метод для создания прикреплённых графических элементов нужен для того, чтобы мы могли при обращении к полю создавать на нём подчинённые объекты.
В защищённой секции класса объявим защищённый конструктор:
protected: CTabField(const ENUM_GRAPH_ELEMENT_TYPE type, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h); public:
В публичной секции объявим методы для работы с классом и параметрический конструктор:
public: virtual void DrawFrame(void); void SetPageNumber(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value); } int PageNumber(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER); } virtual void Erase(const color colour,const uchar opacity,const bool redraw=false); virtual void Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false); CTabField(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h); };
Все виртуальные методы здесь переопределяют одноимённые методы родительских классов, а методы для установки и возврата номера вкладки, к которой принадлежит поле, просто устанавливают в свойство объекта переданное значение, и возвращают его.
Защищённый и параметрический конструкторы:
CTabField::CTabField(const ENUM_GRAPH_ELEMENT_TYPE type, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CContainer(type,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER; this.SetBorderSizeAll(1); this.SetBorderStyle(FRAME_STYLE_SIMPLE); this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true); this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true); this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN); this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER); this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true); this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN); this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER); this.SetForeColor(CLR_DEF_FORE_COLOR,true); this.SetPaddingAll(3); } CTabField::CTabField(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CContainer(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER; this.SetBorderSizeAll(1); this.SetBorderStyle(FRAME_STYLE_SIMPLE); this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true); this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true); this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN); this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER); this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true); this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN); this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER); this.SetForeColor(CLR_DEF_FORE_COLOR,true); this.SetPaddingAll(3); }
Различие между ними лишь в том, что в защищённый конструктор передаётся тип создаваемого объекта (если от него наследоваться), и этот тип передаётся в родительский объект. В публичном же параметрическом конструкторе тип, передаваемый в родительский класс, указан явно — объект-поле.
В теле конструктора созданному объекту устанавливаются нужные значения некоторых свойств по умолчанию. Остальные свойства объекта устанавливаются в родительских классах.
Метод, находящий и возвращающий указатель на заголовок, соответствующий номеру вкладки:
CWinFormBase *CTabField::GetHeaderObj(void) { CWinFormBase *base=this.GetBase(); if(base==NULL) return NULL; CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL); return(list!=NULL ? list.At(0) : NULL); }
Метод подробно расписан в комментариях к коду. Вкратце: для сопоставления этого поля со своим заголовком, хранящимся в классе, к которому привязан этот объект и объекты-заголовки, нам нужно получить доступ к базовому объекту. Такой метод давно у нас существует. Получаем указатель на базовый объект, и из него получаем список всех привязанных к нему объектов с типом объекта-заголовка вкладки. Фильтруем полученный список так, чтобы в нём остался один объект с номером вкладки, записанном в этом объекте. Таким образом мы нашли соответствующий этому полю заголовок, и указатель на него хранится в полученном списке. Его и возвращаем. Если объект не найден, то метод вернёт NULL.
Метод, рисующий рамку элемента в зависимости от расположения заголовка:
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 : break; case CANV_ELEMENT_ALIGNMENT_RIGHT : break; default: break; } }
Метод подробно расписан в комментариях к коду. На данный момент метод рисует рамку при расположении заголовков сверху и снизу. Справа и слева будем делать в последующих статьях. Вкратце: к полю, например, сверху прилегает заголовок. В месте их соприкосновения не должно быть линии. Можно было рисовать рамку поля при помощи ломанной линии, но тут есть некоторая проблема в количестве рисуемых точек в зависимости от расположения заголовка. Если он находится слева или справа поля, то количество точек линии будет на одну меньше в случае, если заголовок находится не с края поля.
Поэтому проще нарисовать сначала прямоугольник, полностью очерчивающий поле, а затем, получив координаты заголовка, нарисовать линию цветом фона поля там, где заголовок соприкасается с полем. Таким образом мы «сотрём» линию в месте соприкосновения заголовка с полем для получения правильного отображения вкладки.
Виртуальные методы, очищающие элемент с заполнением его цветом и непрозрачностью:
void CTabField::Erase(const color colour,const uchar opacity,const bool redraw=false) { CGCnvElement::Erase(colour,opacity,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFrame(); this.Update(redraw); } void CTabField::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false) { CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw); if(this.BorderStyle()!=FRAME_STYLE_NONE) this.DrawFrame(); this.Update(redraw); }
Методы идентичны точно таким же методам в других классах, или в родительских. А переопределены они здесь для того, чтобы рамка рисовалась именно тем методом, который мы рассмотрели выше.
Метод, создающий новый графический объект:
CGCnvElement *CTabField::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int obj_num, const string descript, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { CGCnvElement *element=NULL; switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); return element; }
Виртуальный метод. Также идентичен одноимённым методам в других классах. Здесь присутствует новый блок кода для создания объекта-поля, класс которого мы и рассматриваем. Точно такие же блоки создания этого объекта мы добавим и в других классах в такие же методы.
При помощи этого метода мы сможем создавать подчинённые объекты на поле вкладки.
Доработаем класс заголовка вкладки в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.
Из приватной секции удалим метод для установки размеров и смещения объекта:
void SetWH(void);
Там же объявим методы для установки строки выделенного заголовка вкладки в корректное положение:
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); void CorrectSelectedRowTop(void); void CorrectSelectedRowBottom(void); void CorrectSelectedRowLeft(void); void CorrectSelectedRowRight(void); protected:
В публичной секции объявим/напишем новые методы и доработаем имеющиеся:
public: CWinFormBase *GetFieldObj(void); bool SetSizeOff(void) { return(CGCnvElement::SetWidth(this.m_width_off) && CGCnvElement::SetHeight(this.m_height_off) ? true : false); } bool SetSizeOn(void) { return(CGCnvElement::SetWidth(this.m_width_on) && CGCnvElement::SetHeight(this.m_height_on) ? true : false); } void SetWidthOff(const int value) { this.m_width_off=value; } void SetHeightOff(const int value) { this.m_height_off=value; } void SetWidthOn(const int value) { this.m_width_on=value; } void SetHeightOn(const int value) { this.m_height_on=value; } bool SetSizes(const int w,const int h); 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; } void SetRow(const int value) { this.m_row=value; } int Row(void) const { return this.m_row; } void SetColumn(const int value) { this.m_col=value; } int Column(void) const { return this.m_col; } void SetTabLocation(const int row,const int col) { this.SetRow(row); this.SetColumn(col); } void SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment) { this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment); } ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void) const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT); } void SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode); } ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);} void SetPageNumber(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value); } int PageNumber(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER); } virtual void SetState(const bool flag); virtual void Erase(const color colour,const uchar opacity,const bool redraw=false); virtual void Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false); virtual void OnMouseEventPostProcessing(void); virtual void SetPaddingLeft(const uint value) { this.SetProperty(CANV_ELEMENT_PROP_PADDING_LEFT,(value<1 ? 0 : value)); } virtual void SetPaddingTop(const uint value) { this.SetProperty(CANV_ELEMENT_PROP_PADDING_TOP,(value<1 ? 0 : value)); } virtual void SetPaddingRight(const uint value) { this.SetProperty(CANV_ELEMENT_PROP_PADDING_RIGHT,(value<1 ? 0 : value)); } virtual void SetPaddingBottom(const uint value) { this.SetProperty(CANV_ELEMENT_PROP_PADDING_BOTTOM,(value<1 ? 0 : value)); } virtual void SetPaddingAll(const uint value) { this.SetPaddingLeft(value); this.SetPaddingTop(value); this.SetPaddingRight(value); this.SetPaddingBottom(value); } virtual void SetPadding(const int left,const int top,const int right,const int bottom) { this.SetPaddingLeft(left); this.SetPaddingTop(top); this.SetPaddingRight(right); this.SetPaddingBottom(bottom); } protected:
Ранее метод SetAlignment() помимо установки свойства, устанавливал и размеры рамки. Рамка здесь всегда одного размера — 1 пиксель, и поэтому менять ничего не нужно — удалим это всё:
void SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment) { this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP) this.SetBorderSize(1,1,1,0); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM) this.SetBorderSize(1,0,1,1); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT) this.SetBorderSize(1,1,0,1); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT) this.SetBorderSize(0,1,1,1); }
Конструкторы класса, защищённый и параметрический:
CTabHeader::CTabHeader(const ENUM_GRAPH_ELEMENT_TYPE type, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CButton(type,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP); this.SetToggleFlag(true); this.SetGroupButtonFlag(true); this.SetText(TypeGraphElementAsString(this.TypeGraphElement())); this.SetForeColor(CLR_DEF_FORE_COLOR,true); this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true); this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true); this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN); this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER); this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true); this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON); this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON); this.SetBorderStyle(FRAME_STYLE_SIMPLE); this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true); this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN); this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER); this.SetPadding(6,3,6,3); this.SetSizes(w,h); this.SetState(false); } CTabHeader::CTabHeader(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CButton(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP); this.SetToggleFlag(true); this.SetGroupButtonFlag(true); this.SetText(TypeGraphElementAsString(this.TypeGraphElement())); this.SetForeColor(CLR_DEF_FORE_COLOR,true); this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true); this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true); this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN); this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER); this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true); this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON); this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON); this.SetBorderStyle(FRAME_STYLE_SIMPLE); this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true); this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN); this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER); this.SetPadding(6,3,6,3); this.SetSizes(w,h); this.SetState(false); }
Теперь, вместо раздельной установки размеров заголовка:
this.SetWidthOff(this.Width());
this.SetHeightOff(this.Height());
this.SetWidthOn(this.Width()+4);
this.SetHeightOn(this.Height()+2);
вызывается метод установки размеров заголовка вкладки, где устанавливаются размеры в зависимости от режима задания размера заголовка:
bool CTabHeader::SetSizes(const int w,const int h) { int width=(w<4 ? 4 : w); int height=(h<4 ? 4 : h); switch(this.TabSizeMode()) { case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : this.TextSize(this.Text(),width,height); width+=this.PaddingLeft()+this.PaddingRight(); height=h+this.PaddingTop()+this.PaddingBottom(); break; case CANV_ELEMENT_TAB_SIZE_MODE_FIXED : break; default: break; } bool res=true; res &=this.SetWidth(width); res &=this.SetHeight(height); if(!res) return false; this.SetWidthOn(this.Width()+4); this.SetHeightOn(this.Height()+2); this.SetWidthOff(this.Width()); this.SetHeightOff(this.Height()); return true; }
Логика метода расписана в комментариях к коду. Размеры подстраиваются только для режима, где ширина заголовка соответствует ширине выводимого на нём текста. Для режима Fixed размер заголовка должен быть фиксированным, поэтому он остаётся тем, который был передан в метод в переменных w и h, но скорректированным, если переданы размеры меньше четырёх пикселей (в переменных width и height). Режим растягивания ширины по размеру контейнера будем делать в последующих статьях.
Метод, устанавливающий состояние элемента управления, претерпел сильные изменения:
void CTabHeader::SetState(const bool flag) { bool state=this.State(); CButton::SetState(flag); if(state!=this.State()) { if(this.State()) { this.WHProcessStateOn(); this.BringToTop(); CWinFormBase *base=this.GetBase(); if(base==NULL) return; base.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber()); CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); if(list==NULL) return; for(int i=0;i<list.Total();i++) { CWinFormBase *obj=list.At(i); if(obj==NULL || obj.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)==this.PageNumber()) continue; obj.SetZorder(base.Zorder(),false); obj.Hide(); } CWinFormBase *field=this.GetFieldObj(); if(field==NULL) return; field.Show(); field.SetZorder(base.Zorder()+1,false); field.DrawFrame(); field.BringToTop(); } else this.WHProcessStateOff(); } }
Если кнопка выбрана (щёлкнули по заголовку вкладки), то нам нужно увеличить размер кнопки (заголовка) и вывести заголовок на передний план. Затем нам нужно скрыть все поля вкладок, не соответствующие выбранному заголовку, а поле этого заголовка наоборот — отобразить и вывести на передний план. Кроме того, отображённое поле вкладки должно стать доступным для щелчка мышкой, поэтому его параметр ZOrder нужно сделать больше, чем у остальных объектов элемента управления, а у невыбранных полей — наоборот, сделать ZOrder ниже, чем у выбранного. Это всё и делает этот метод.
В методе, подстраивающем размер и расположение элемента в состоянии «выбран» в зависимости от его расположения, нужно вызвать методы, которые сместят строку выбранного заголовка в положение, в котором заголовок будет присоединён к своему полю — ведь в случае, если у нас разрешено расположение заголовков в несколько строк, выбранный заголовок может находиться в ряду, не соприкасающимся с полем:
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()); } case CANV_ELEMENT_ALIGNMENT_LEFT : this.CorrectSelectedRowLeft(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.CorrectSelectedRowRight(); 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 : break; case CANV_ELEMENT_ALIGNMENT_RIGHT : break; default: break; } return true; }
Это задел на последующие доработки.
Метод, устанавливающий строку выделенного заголовка вкладки в корректное положение сверху:
void CTabHeader::CorrectSelectedRowTop(void) { int row_pressed=this.Row(); int y_pressed=this.CoordY(); int y0=0; if(row_pressed==0) return; CWinFormBase *obj=this.GetFieldObj(); if(obj==NULL) return; y0=obj.CoordY()-this.Height()+2; 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()); 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()); header.SetRow(-2); } } } for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; if(header.Row()==-1) header.SetRow(row_pressed); if(header.Row()==-2) header.SetRow(0); } }
Логика метода полностью расписана в комментариях к коду. Суть такая: если мы выбрали вкладку (нажали на кнопку-заголовок вкладки), находящуюся в нулевой строке (нулевая соприкасается с полем вкладки, первая — находится над нулевой, вторая — над первой, и т.д.), то строку переносить на новое место не нужно — она и так находится на своём месте. Если же мы выбрали вкладку, заголовок которой находится не в нулевой строке, то нам нужно все заголовки этой строки переместить на место нулевой, а нулевую строку переместить на место той, по заголовку которой мы щёлкнули мышкой. Таким образом всегда будут меняться местами строки нулевая и та, в которой находится заголовок выбранной вкладки.
Этот метод обрабатывает только ситуацию, когда заголовки вкладок находятся сверху. Они так же могут находиться снизу, слева и справа. Но обработчики этих ситуаций будем делать в последующих статьях. Сейчас же просто напишем методы-заглушки для них:
void CTabHeader::CorrectSelectedRowBottom(void) { } void CTabHeader::CorrectSelectedRowLeft(void) { } void CTabHeader::CorrectSelectedRowRight(void) { }
Метод, ищущий и возвращающий указатель на объект поля, соответствующий номеру вкладки:
CWinFormBase *CTabHeader::GetFieldObj(void) { CWinFormBase *base=this.GetBase(); if(base==NULL) return NULL; CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL); return(list!=NULL ? list.At(0) : NULL); }
Метод идентичен методу GetHeaderObj(), ищущему и возвращающему указатель на заголовок вкладки, рассмотренному нами выше в классе объекта-поля вкладки. Этот метод ищет поле вкладки, соответствующее этому заголовку.
В обработчике события «Курсор в пределах активной области, отжата кнопка мышки (левая)», добавим блок кода, где для заголовка, по которому был щелчок мышки, ищется соответствующее ему поле вкладки и отображается:
void CTabHeader::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorInit(),false); this.SetForeColor(this.ForeColorInit(),false); } else { this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false); this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false); } this.SetBorderColor(this.BorderColorInit(),false); Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } else { if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.SetForeColor(this.ForeColorMouseOver(),false); this.Redraw(true); } else { if(!this.GroupButtonFlag()) this.SetState(!this.State()); else if(!this.State()) this.SetState(true); this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false); this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false); CWinFormBase *field=this.GetFieldObj(); if(field!=NULL) { field.Show(); field.BringToTop(); field.DrawFrame(); } this.Redraw(true); } Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group()); this.SetBorderColor(this.BorderColorMouseOver(),false); } }
Если мы щёлкаем по заголовку, то результатом должно стать отображение соответствующего ему поля вкладки. Здесь выделенный цветом блок кода это и делает. Для простой кнопки (в дальнейшем будем делать вид заголовков, и там будет отображение в виде кнопок) добавим перерисовку графика. Честно — не помню в результате какого из экспериментов появилась эта строка. Но пока пусть будет — мы сюда пока ещё не попадаем.
Всё управление заголовками и полями вкладок должно осуществляться из класса элемента управления TabControl.
Доработаем класс в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh.
Подключим к файлу файл только что написанного класса объекта-поля вкладки и в приватной секции объявим новые переменные и методы:
#property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/ru/users/artmedia70" #property version "1.00" #property strict #include "Container.mqh" #include "GroupBox.mqh" #include "..\TabHeader.mqh" #include "..\TabField.mqh" class CTabControl : public CContainer { private: int m_item_width; int m_item_height; int m_header_padding_x; int m_header_padding_y; int m_field_padding_top; int m_field_padding_bottom; int m_field_padding_left; int m_field_padding_right; virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string descript, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); CArrayObj *GetListHeaders(void) { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); } CArrayObj *GetListFields(void) { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); } void SetSelected(const int index); void SetUnselected(const int index); void SetSelectedTabPageNum(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value); } void ArrangeTabHeaders(void); void ArrangeTabHeadersTop(void); void ArrangeTabHeadersBottom(void); void ArrangeTabHeadersLeft(void); void ArrangeTabHeadersRight(void); public:
В публичной секции класса объявим новые методы:
public: bool CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text=""); bool CreateNewElement(const int tab_page, const ENUM_GRAPH_ELEMENT_TYPE element_type, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity, const bool redraw); int TabElementsTotal(const int tab_page); CGCnvElement *GetTabElement(const int tab_page,const int index); CArrayObj *GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type); int TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type); CGCnvElement *GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index); CTabHeader *GetTabHeader(const int index) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index); } CWinFormBase *GetTabField(const int index) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,index); } int TabPages(void) { return(this.GetListHeaders()!=NULL ? this.GetListHeaders().Total() : 0); } void SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment) { this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment); CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; header.SetAlignment(alignment); } } ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void) const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT); } void SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode) { this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode); CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; header.SetTabSizeMode(mode); } } ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);} void SetHeaderPadding(const int w,const int h); void SetFieldPadding(const int top,const int bottom,const int left,const int right); int HeaderPaddingWidth(void) const { return this.m_header_padding_x; } int HeaderPaddingHeight(void) const { return this.m_header_padding_y; } int FieldPaddingTop(void) const { return this.m_field_padding_top; } int FieldPaddingBottom(void) const { return this.m_field_padding_bottom; } int FieldPaddingLeft(void) const { return this.m_field_padding_left; } int FieldPaddingRight(void) const { return this.m_field_padding_right; } void SetMultiline(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,flag); } bool Multiline(void) const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE); } void SetItemWidth(const int value) { this.m_item_width=value; } int ItemWidth(void) const { return this.m_item_width; } void SetItemHeight(const int value) { this.m_item_height=value; } int ItemHeight(void) const { return this.m_item_height; } void SetItemSize(const int w,const int h) { if(this.ItemWidth()!=w) this.SetItemWidth(w); if(this.ItemHeight()!=h) this.SetItemHeight(h); } void SetHeaderText(CTabHeader *header,const string text); void SetHeaderText(const int index,const string text); void Select(const int index,const bool flag); int SelectedTabPageNum(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);} CWinFormBase *SelectedTabPage(void) { return this.GetTabField(this.SelectedTabPageNum()); } CTabControl(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h); };
В конструкторе класса установим значения по умолчанию для режима задания размера вкладок и установим значения Padding для заголовков и полей:
CTabControl::CTabControl(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h) : CContainer(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL); this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER; this.SetBorderSizeAll(0); this.SetBorderStyle(FRAME_STYLE_NONE); this.SetOpacity(0,true); this.SetBackgroundColor(CLR_CANV_NULL,true); this.SetBackgroundColorMouseDown(CLR_CANV_NULL); this.SetBackgroundColorMouseOver(CLR_CANV_NULL); this.SetBorderColor(CLR_CANV_NULL,true); this.SetBorderColorMouseDown(CLR_CANV_NULL); this.SetBorderColorMouseOver(CLR_CANV_NULL); this.SetForeColor(CLR_DEF_FORE_COLOR,true); this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP); this.SetItemSize(58,18); this.SetTabSizeMode(CANV_ELEMENT_TAB_SIZE_MODE_NORMAL); this.SetHeaderPadding(6,3); this.SetFieldPadding(3,3,3,3); }
В методе, создающем указанное количество вкладок, установим в создаваемые объекты заголовков и полей указатель на базовый объект, номер вкладки и группу. Для заголовков и полей установим значение Padding, добавим текст вкладки, если в метод передан пустой текст для установки в заголовки вкладок, установим режим задания размера заголовков и установим их размеры:
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; if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP) header_y=0; if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM) header_y=this.Height()-h; header_x=(header==NULL ? header_x : header.RightEdgeRelative()); if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,w,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)); header.SetTabSizeMode(this.TabSizeMode()); header.SetSizes(w,h); int field_x=0; int field_y=0; int field_h=this.Height()-header.Height(); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP) field_y=header.BottomEdgeRelative(); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM) field_y=0; if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,this.Width(),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; }
После создания указанного количества вкладок вызовем метод, располагающий заголовки в соответствии с установленными режимами их отображения.
Метод, создающий новый присоединённый элемент:
bool CTabControl::CreateNewElement(const int tab_page, const ENUM_GRAPH_ELEMENT_TYPE element_type, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool activity, const bool redraw) { CTabField *field=this.GetTabField(tab_page); if(field==NULL) { CMessage::ToLog(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ); ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ)," (Tab",(string)tab_page,")"); return false; } return field.CreateNewElement(element_type,x,y,w,h,colour,opacity,activity,redraw); }
Получаем объект-поле вкладки по указанному номеру вкладки и возвращаем результат вызова его метода создания нового присоединённого элемента.
Метод, располагающий заголовки вкладок в соответствии с установленными режимами:
void CTabControl::ArrangeTabHeaders(void) { switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : this.ArrangeTabHeadersTop(); break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.ArrangeTabHeadersBottom(); break; case CANV_ELEMENT_ALIGNMENT_LEFT : this.ArrangeTabHeadersLeft(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.ArrangeTabHeadersRight(); break; default: break; } }
В зависимости от установленного режима расположения вкладок, вызываем соответствующие методы.
Метод, располагающий заголовки вкладок сверху:
void CTabControl::ArrangeTabHeadersTop(void) { CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; int col=0; int row=0; int x1_base=2; int x2_base=this.RightEdgeRelative()-2; 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()) { if(this.TabSizeMode()<CANV_ELEMENT_TAB_SIZE_MODE_FILL) { int x2=header.RightEdgeRelative()-x_shift; if(x2<x2_base) col=i-n; else { 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 { } } else { } } CTabHeader *last=this.GetTabHeader(list.Total()-1); if(last!=NULL && last.Row()>0) { 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); } } } }
Логика метода полностью расписана в комментариях к коду. Метод пока реализован только для расположения заголовков наверху контейнера в несколько рядов. Здесь мы проверяем помещается ли каждый очередной заголовок в цикле на следующую позицию в строке так, чтобы он не выходил за край контейнера. Если выходит — значит нужно начать новую строку выше предыдущей. Пересчитываем начало координат отсчёта так, чтобы при обращении к координатам объекта минус рассчитанное смещение, строка как бы начиналась опять от левого края контейнера. и далее опять рассчитывается вмещаются ли объекты внутри контейнера и, если нет, то опять переходим на новую строку. После каждой новой строки в объекты записываются увеличенные значения Row (строка), а подсчёт значений Col (колонка/столбец) начинается заново. По окончании цикла мы имеем список, в котором прописаны значения рядов и столбцов, в которых должны располагаться заголовки.
Далее в новом цикле по списку заголовков мы их располагаем в новых координатах, соответствующих записанным в объекте значениям ряда и столбца, а соответствующие им объекты-поля вкладок перемещаются на дистанцию, рассчитанную от максимального значения ряда, и на эту же величину уменьшаются по высоте. После завершения цикла получим правильно расположенные заголовки и соответствующие им поля.
В последующих статьях дополним метод расположением заголовков при остальных режимах.
Остальные подобные методы реализованы пока в качестве методов-заглушек:
void CTabControl::ArrangeTabHeadersBottom(void) { } void CTabControl::ArrangeTabHeadersLeft(void) { } void CTabControl::ArrangeTabHeadersRight(void) { }
Метод, устанавливающий всем заголовкам вкладок значения Padding:
void CTabControl::SetHeaderPadding(const int w,const int h) { this.m_header_padding_x=w; this.m_header_padding_y=h; CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; header.SetPadding(this.m_header_padding_x,this.m_header_padding_y,this.m_header_padding_x,this.m_header_padding_y); } }
В метод передаются значения, дополнительно добавляемые к ширине и высоте заголовка при режиме задания размера Normal. Переданные в метод значения сразу же записываются в соответствующие переменные. Далее получаем список всех заголовков и в цикле по полученному списку каждому заголовку из списка устанавливаем переданные в метод значения Padding.
Метод, устанавливающий всем полям вкладок значения Padding:
void CTabControl::SetFieldPadding(const int top,const int bottom,const int left,const int right) { this.m_field_padding_top=top; this.m_field_padding_bottom=bottom; this.m_field_padding_left=left; this.m_field_padding_right=right; CArrayObj *list=this.GetListFields(); if(list==NULL) return; for(int i=0;i<list.Total();i++) { CTabField *field=list.At(i); if(field==NULL) continue; field.SetPadding(left,top,right,bottom); } }
Метод аналогичен вышерассмотренному. Но здесь в него передаются значения Padding сверху, снизу, справа и слева поля вкладки. Эти значения устанавливаются в соответствующие переменные, а затем в цикле — и в каждый объект-поле вкладки.
Метод, устанавливающий вкладку выбранной, теперь переработан:
void CTabControl::SetSelected(const int index) { CTabHeader *header=this.GetTabHeader(index); if(header==NULL) return; if(!header.State()) header.SetState(true); this.SetSelectedTabPageNum(index); }
Теперь все манипуляции с перемещением объекта на передний план и выборе соответствующего ему поля, производятся в методе SetState() класса объекта-заголовка вкладки, рассмотренного нами выше.
Метод, устанавливающий вкладку не выбранной, переработан аналогичным образом:
void CTabControl::SetUnselected(const int index) { CTabHeader *header=this.GetTabHeader(index); if(header==NULL) return; if(header.State()) header.SetState(false); }
Метод, возвращающий количество привязанных элементов в указанной вкладке:
int CTabControl::TabElementsTotal(const int tab_page) { CTabField *field=this.GetTabField(tab_page); return(field!=NULL ? field.ElementsTotal() : 0); }
Получаем объект-поле вкладки по указанному номеру и возвращаем количество привязанных к нему объектов.
Метод позволяет узнать сколько объектов прикреплено ко вкладке с указанным номером.
Метод, возвращающий привязанный элемент по индексу в списке в указанной вкладке:
CGCnvElement *CTabControl::GetTabElement(const int tab_page,const int index) { CTabField *field=this.GetTabField(tab_page); return(field!=NULL ? field.GetElement(index) : NULL); }
Получаем объект-поле по указанному номеру и возвращаем указатель на прикреплённый элемент из списка по указанному индексу.
Метод позволяет получить указатель на нужный элемент по его индексу на указанной вкладке.
Метод, возвращающий по типу объекта список привязанных элементов в указанной вкладке:
CArrayObj *CTabControl::GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type) { CTabField *field=this.GetTabField(tab_page); return(field!=NULL ? field.GetListElementsByType(type) : NULL); }
Получаем объект-поле по указанному номеру и возвращаем список привязанных элементов по указанному типу.
Метод позволяет получить список элементов одного указанного типа с нужной вкладки.
Метод, возвращающий по типу объекта количество привязанных элементов в указанной вкладке:
int CTabControl::TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type) { CTabField *field=this.GetTabField(tab_page); return(field!=NULL ? field.ElementsTotalByType(type) : 0); }
Получаем объект-поле по указанному номеру и возвращаем количество элементов указанного типа, расположенных на вкладке.
Метод позволяет узнать сколько элементов указанного типа размещено на указанной вкладке.
Метод, возвращающий по типу объекта привязанный элемент по индексу в списке в указанной вкладке:
CGCnvElement *CTabControl::GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index) { CTabField *field=this.GetTabField(tab_page); return(field!=NULL ? field.GetElementByType(type,index) : NULL); }
Получаем объект-поле по указанному номеру и возвращаем элемент нужного типа по указанному индексу в списке.
Метод позволяет получить элемент нужного типа по его номеру с указанной вкладки.
В методе, создающем новый графический объект, в самом его конце, добавим блок кода для создания объекта-поля вкладки (отрывок кода):
case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); return element; }
К файлу \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh класса объекта-панели подключим файл класса объекта-поля вкладки:
#property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/ru/users/artmedia70" #property version "1.00" #property strict #include "Container.mqh" #include "..\TabField.mqh" #include "GroupBox.mqh" #include "TabControl.mqh" #include "..\..\WForms\Common Controls\ListBox.mqh" #include "..\..\WForms\Common Controls\CheckedListBox.mqh" #include "..\..\WForms\Common Controls\ButtonListBox.mqh"
В конце метода, создающего новый графический объект, пропишем блок кода для создания объекта-поля вкладки:
case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); return element; }
Теперь в объекте-панели и его наследниках можно будет создавать объекты этого типа.
В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh класса базового объекта-контейнера, в методе, устанавливающем параметры присоединённому объекту, впишем установку параметров объекта-поля вкладки в блок кода установки параметров объектов Button, TabHeader и ListBoxItem (отрывок кода):
case GRAPH_ELEMENT_TYPE_WF_LABEL : case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBackgroundColor(CLR_CANV_NULL,true); obj.SetOpacity(0,false); break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM : obj.SetForeColor(this.ForeColor(),true); obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBorderStyle(FRAME_STYLE_SIMPLE); break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX : case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX :
После создания объекта в этом методе, ему будут назначены указанные здесь свойства. В последующем их можно будет поменять.
В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh класса объекта GroupBox сделаем метод рисования рамки виртуальным:
class CGroupBox : public CContainer { private: virtual void DrawFrame(void);
Так как мы объявили такой метод в базовом объекте всех WinForms-объектов виртуальным, то теперь все такие одноимённые методы в классах-наследниках тоже нужно делать виртуальными — для правильного их переопределения и обращения к ним из других классов.
В методе создания нового графического объекта тоже добавим блок кода для создания объекта-поля вкладки:
case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break; default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); return element; }
В классе-коллекции графических элементов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh в методах создания графических элементов на канвасе были заменены имена переменных с «name» на «descript» — это отголоски изменения алгоритма именования графических элементов в прошлой статье. Оно и так работало без ошибок, так как тип переменной string, но для правильности наименований формальных параметров методов, они были изменены на «описание» вместо «имя», что правильно. В качестве примера:
int CreateFormHGradientCicle(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h, color &clr[], const uchar opacity, const bool movable, const bool activity, const bool shadow=false, const bool redraw=false) { int id=this.GetMaxID()+1; CForm *obj=new CForm(chart_id,subwindow,descript,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetBackgroundColors(clr,true); obj.SetBorderColor(clr[0],true); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.BorderColor(),obj.Opacity()); obj.Done(); obj.Erase(clr,opacity,false,true,redraw); return obj.ID(); }
Остальные изменения мы тут рассматривать не будем — они идентичны, уже все внесены в файл библиотеки, с которым можно ознакомиться в прилагаемых к статье файлах.
В методе, ищущем объекты взаимодействия, добавим проверку на видимость и доступность объекта. Если он невидим или недоступен — такой объект обрабатывать нельзя — он должен быть недоступным для взаимодействия с мышкой:
CForm *CGraphElementsCollection::SearchInteractObj(CForm *form,const int id,const long &lparam,const double &dparam,const string &sparam) { if(form!=NULL) { int total=form.CreateListInteractObj(); for(int i=total-1;i>WRONG_VALUE;i--) { CForm *obj=form.GetInteractForm(i); if(obj==NULL) continue; if(!obj.IsVisible()) { continue; } if(!obj.Enabled()) { continue; } if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL) { CTabControl *tab_ctrl=obj; CForm *elm=tab_ctrl.SelectedTabPage(); if(elm!=NULL && elm.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) return elm; } if(obj.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) return obj; } } return form; }
Здесь: если объект скрыт или недоступен — пропускаем его. Если это объект TabControl, то получаем из него выбранную вкладку.
Если курсор находится над выбранной вкладкой — возвращаем указатель на объект-поле вкладки.
В методе постобработки бывшей активной формы под курсором пропустим все скрытые и недоступные объекты — их обрабатывать не нужно:
void CGraphElementsCollection::FormPostProcessing(CForm *form,const int id, const long &lparam, const double &dparam, const string &sparam) { CForm *main=form.GetMain(); if(main==NULL) main=form; CArrayObj *list=main.GetListElements(); if(list==NULL) return; int total=list.Total(); for(int i=0;i<total;i++) { CForm *obj=list.At(i); if(obj==NULL) continue; obj.OnMouseEventPostProcessing(); int count=obj.CreateListInteractObj(); for(int j=0;j<count;j++) { CWinFormBase *elm=obj.GetInteractForm(j); if(elm==NULL || !elm.IsVisible() || !elm.Enabled()) continue; elm.MouseFormState(id,lparam,dparam,sparam); elm.OnMouseEventPostProcessing(); } } ::ChartRedraw(main.ChartID()); }
В файле \MQL5\Include\DoEasy\Engine.mqh главного объекта библиотеки переименуем метод GetWFPanel(), возвращающий объект по имени, в GetWFPanelByName(), а метод GetWFPanel() сделаем возвращающим объект по его описанию:
CPanel *GetWFPanelByName(const string name) { string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); return(list!=NULL ? list.At(0) : NULL); } CPanel *GetWFPanel(const string descript) { CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_DESCRIPTION,descript,EQUAL); return(list!=NULL ? list.At(0) : NULL); }
Так как у обоих методов были формальные параметры одинакового типа, то в такой ситуации перегрузка методов невозможна. Именно по этой причине мы переименовали один из методов.
Точно так же, как и в классе-коллекции графических элементов, все вхождения «name» в формальных параметрах методов, создающих WinForms-объекты, переименованы в «descript«.
Для примера:
CGCnvElement *CreateWFElement(const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w, const int h, color &clr[], const uchar opacity, const bool v_gradient=true, const bool c_gradient=false, const bool redraw=false) { int obj_id= ( v_gradient ? ( !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) : this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) ) : !v_gradient ? ( !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) : this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) ) : WRONG_VALUE ); return this.GetWFElement(obj_id); }
На сегодня это все изменения и доработки.
Тестирование
Для теста возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part115\ под новым именем TestDoEasy115.mq5.
Чтобы можно было выбрать режим установки размеров заголовкам вкладок, и при этом при английской версии компиляции константы перечислений были на английском языке, а при русской — на русском, создадим новые перечисления для условной компиляции:
enum ENUM_ELEMENT_ALIGNMENT { ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP, ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM, ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT, ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT, }; enum ENUM_ELEMENT_TAB_SIZE_MODE { ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL, ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED, ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL, }; #else enum ENUM_AUTO_SIZE_MODE { AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW, AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK }; enum ENUM_BORDER_STYLE { BORDER_STYLE_NONE=FRAME_STYLE_NONE, BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE, BORDER_STYLE_FLAT=FRAME_STYLE_FLAT, BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL, BORDER_STYLE_STAMP=FRAME_STYLE_STAMP, }; enum ENUM_CHEK_STATE { CHEK_STATE_UNCHECKED=CANV_ELEMENT_CHEK_STATE_UNCHECKED, CHEK_STATE_CHECKED=CANV_ELEMENT_CHEK_STATE_CHECKED, CHEK_STATE_INDETERMINATE=CANV_ELEMENT_CHEK_STATE_INDETERMINATE, }; enum ENUM_ELEMENT_ALIGNMENT { ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP, ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM, ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT, ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT, }; enum ENUM_ELEMENT_TAB_SIZE_MODE { ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL, ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED, ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL, }; #endif
Во входные параметры советника добавим новую переменную, задающую режим установки размеров заголовкам вкладок:
sinput bool InpMovable = true; sinput ENUM_INPUT_YES_NO InpAutoSize = INPUT_YES; sinput ENUM_AUTO_SIZE_MODE InpAutoSizeMode = AUTO_SIZE_MODE_GROW; sinput ENUM_BORDER_STYLE InpFrameStyle = BORDER_STYLE_SIMPLE; sinput ENUM_ANCHOR_POINT InpTextAlign = ANCHOR_CENTER; sinput ENUM_INPUT_YES_NO InpTextAutoSize = INPUT_NO; sinput ENUM_ANCHOR_POINT InpCheckAlign = ANCHOR_LEFT; sinput ENUM_ANCHOR_POINT InpCheckTextAlign = ANCHOR_LEFT; sinput ENUM_CHEK_STATE InpCheckState = CHEK_STATE_UNCHECKED; sinput ENUM_INPUT_YES_NO InpCheckAutoSize = INPUT_YES; sinput ENUM_BORDER_STYLE InpCheckFrameStyle = BORDER_STYLE_NONE; sinput ENUM_ANCHOR_POINT InpButtonTextAlign = ANCHOR_CENTER; sinput ENUM_INPUT_YES_NO InpButtonAutoSize = INPUT_YES; sinput ENUM_AUTO_SIZE_MODE InpButtonAutoSizeMode= AUTO_SIZE_MODE_GROW; sinput ENUM_BORDER_STYLE InpButtonFrameStyle = BORDER_STYLE_NONE; sinput bool InpButtonToggle = true ; sinput bool InpListBoxMColumn = true; sinput bool InpButtListMSelect = false; sinput ENUM_ELEMENT_TAB_SIZE_MODE InpTabPageSizeMode = ELEMENT_TAB_SIZE_MODE_NORMAL;
Создание WinForms-объектов в обработчике OnInit() советника теперь будет таким:
int OnInit() { ArrayResize(array_clr,2); array_clr[0]=C'26,100,128'; array_clr[1]=C'35,133,169'; string array[1]={Symbol()}; engine.SetUsedSymbols(array); engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",50,50,400,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false); if(pnl!=NULL) { pnl.SetPaddingAll(4); pnl.SetMovable(InpMovable); pnl.SetAutoSize(InpAutoSize,false); pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false); CPanel *obj=NULL; for(int i=0;i<2;i++) { CPanel *prev=pnl.GetElement(i-1); int xb=0, yb=0; int x=(prev==NULL ? xb : xb+prev.Width()+20); int y=0; if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false)) { obj=pnl.GetElement(i); if(obj==NULL) continue; obj.SetBorderSizeAll(3); obj.SetBorderStyle(FRAME_STYLE_BEVEL); obj.SetBackgroundColor(obj.ChangeColorLightness(obj.BackgroundColor(),4*i),true); obj.SetForeColor(clrRed,true); int w=obj.Width()-obj.BorderSizeLeft()-obj.BorderSizeRight(); int h=obj.Height()-obj.BorderSizeTop()-obj.BorderSizeBottom(); obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,0,0,w,h,clrNONE,255,false,false); CLabel *lbl=obj.GetElement(0); if(lbl!=NULL) { if(i % 2==0) lbl.SetForeColor(CLR_DEF_FORE_COLOR,true); else lbl.SetForeColorOpacity(127); lbl.SetFontBoldType(FW_TYPE_BLACK); lbl.SetTextAlign(InpTextAlign); lbl.SetAutoSize((bool)InpTextAutoSize,false); lbl.SetText(GetPrice(i % 2==0 ? SYMBOL_BID : SYMBOL_ASK)); lbl.SetBorderSizeAll(1); lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle); lbl.SetBorderColor(CLR_DEF_BORDER_COLOR,true); lbl.Update(true); } } } CGroupBox *gbox1=NULL; int w=pnl.GetUnderlay().Width(); int y=obj.BottomEdgeRelative()+6; if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0,y,200,150,C'0x91,0xAA,0xAE',0,true,false)) { gbox1=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0); if(gbox1!=NULL) { gbox1.SetBorderStyle(FRAME_STYLE_STAMP); gbox1.SetBorderColor(pnl.BackgroundColor(),true); gbox1.SetForeColor(gbox1.ChangeColorLightness(obj.BackgroundColor(),-1),true); gbox1.SetText("GroupBox1"); gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,2,10,50,20,clrNONE,255,true,false); CCheckBox *cbox=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,0); if(cbox!=NULL) { cbox.SetAutoSize((bool)InpCheckAutoSize,false); cbox.SetCheckAlign(InpCheckAlign); cbox.SetTextAlign(InpCheckTextAlign); cbox.SetText("CheckBox"); cbox.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); cbox.SetBorderColor(CLR_DEF_BORDER_COLOR,true); cbox.SetChecked(true); cbox.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)InpCheckState); } CRadioButton *rbtn=NULL; for(int i=0;i<4;i++) { int yrb=(rbtn==NULL ? cbox.BottomEdgeRelative() : rbtn.BottomEdgeRelative()); gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,2,yrb+4,50,20,clrNONE,255,true,false); rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i); if(rbtn!=NULL) { rbtn.SetAutoSize((bool)InpCheckAutoSize,false); rbtn.SetCheckAlign(InpCheckAlign); rbtn.SetTextAlign(InpCheckTextAlign); rbtn.SetText("RadioButton"+string(i+1)); rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true); rbtn.SetChecked(!i); rbtn.SetGroup(2); } } CButton *butt=NULL; for(int i=0;i<3;i++) { int ybtn=(butt==NULL ? 12 : butt.BottomEdgeRelative()+4); gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,(int)fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+20,ybtn,78,18,clrNONE,255,true,false); butt=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i); if(butt!=NULL) { butt.SetAutoSize((bool)InpButtonAutoSize,false); butt.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpButtonAutoSizeMode,false); butt.SetTextAlign(InpButtonTextAlign); butt.SetForeColor(butt.ChangeColorLightness(CLR_DEF_FORE_COLOR,2),true); butt.SetBorderStyle((ENUM_FRAME_STYLE)InpButtonFrameStyle); butt.SetBorderColor(butt.ChangeColorLightness(butt.BackgroundColor(),-10),true); butt.SetToggleFlag(InpButtonToggle); string txt="Button"+string(i+1); if(butt.Toggle()) butt.SetText("Toggle-"+txt); else butt.SetText(txt); if(i<2) { butt.SetGroup(2); if(butt.Toggle()) { butt.SetBackgroundColorMouseOver(butt.ChangeColorLightness(butt.BackgroundColor(),-5)); butt.SetBackgroundColorMouseDown(butt.ChangeColorLightness(butt.BackgroundColor(),-10)); butt.SetBackgroundStateOnColor(C'0xE2,0xC5,0xB1',true); butt.SetBackgroundStateOnColorMouseOver(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-5)); butt.SetBackgroundStateOnColorMouseDown(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-10)); } } } } rbtn=NULL; for(int i=0;i<2;i++) { int yrb=(rbtn==NULL ? butt.BottomEdgeRelative() : rbtn.BottomEdgeRelative()); gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,butt.CoordXRelative()-4,yrb+3,50,20,clrNONE,255,true,false); rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i+4); if(rbtn!=NULL) { rbtn.SetAutoSize((bool)InpCheckAutoSize,false); rbtn.SetCheckAlign(InpCheckAlign); rbtn.SetTextAlign(InpCheckTextAlign); rbtn.SetText("RadioButton"+string(i+5)); rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true); rbtn.SetChecked(!i); rbtn.SetGroup(3); } } } } CGroupBox *gbox2=NULL; w=gbox1.Width()-1; int x=gbox1.RightEdgeRelative()+1; int h=gbox1.BottomEdgeRelative()-6; if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false)) { gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,1); if(gbox2!=NULL) { gbox2.SetBorderStyle(FRAME_STYLE_STAMP); gbox2.SetBorderColor(pnl.BackgroundColor(),true); gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-1),true); gbox2.SetText("GroupBox2"); gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false); CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0); if(tab_ctrl!=NULL) { tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode); tab_ctrl.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP); tab_ctrl.SetMultiline(true); tab_ctrl.SetHeaderPadding(6,0); tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage")); tab_ctrl.CreateNewElement(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,4,12,160,20,clrNONE,255,true,false); CCheckedListBox *check_lbox=tab_ctrl.GetTabElementByType(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,0); if(check_lbox!=NULL) { check_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true); check_lbox.SetMultiColumn(InpListBoxMColumn); check_lbox.SetColumnWidth(0); check_lbox.CreateCheckBox(4,66); } tab_ctrl.CreateNewElement(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,4,12,160,30,clrNONE,255,true,false); CButtonListBox *butt_lbox=tab_ctrl.GetTabElementByType(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,0); if(butt_lbox!=NULL) { butt_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true); butt_lbox.SetMultiColumn(true); butt_lbox.SetColumnWidth(0); butt_lbox.CreateButton(4,66,16); butt_lbox.SetMultiSelect(InpButtListMSelect); butt_lbox.SetToggle(InpButtonToggle); for(int i=0;i<butt_lbox.ElementsTotal();i++) { butt_lbox.SetButtonGroup(i,(i % 2==0 ? butt_lbox.Group()+1 : butt_lbox.Group()+2)); butt_lbox.SetButtonGroupFlag(i,(i % 2==0 ? true : false)); } } int lbw=146; if(!InpListBoxMColumn) lbw=100; tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,12,lbw,60,clrNONE,255,true,false); CListBox *list_box=tab_ctrl.GetTabElementByType(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,0); if(list_box!=NULL) { list_box.SetBackgroundColor(tab_ctrl.BackgroundColor(),true); list_box.SetMultiColumn(InpListBoxMColumn); list_box.CreateList(8,68); } for(int i=3;i<9;i++) { CTabField *field=tab_ctrl.GetTabField(i); if(field==NULL) continue; tab_ctrl.CreateNewElement(i,GRAPH_ELEMENT_TYPE_WF_LABEL,1,1,field.Width()-2,field.Height()-2,clrNONE,255,true,false); CLabel *label=tab_ctrl.GetTabElementByType(i,GRAPH_ELEMENT_TYPE_WF_LABEL,0); if(label!=NULL) { label.SetTextAlign(ANCHOR_CENTER); label.SetText(tab_ctrl.GetTabHeader(i).Text()); } } } } } pnl.Redraw(true); } return(INIT_SUCCEEDED); }
Надеюсь, вся последовательность создания объектов достаточно ясно расписана в коментариях к коду. Здесь мы на втором контейнере GroupBox создаём элемент управления TabControl с девятью вкладками — специально, чтобы проверить как они будут располагаться в ряды. На первых трёх вкладках создадим объекты, ранее создаваемые нами на контейнере GroupBox2. Теперь же все эти три элемента управления будут размещены каждый на своей вкладке. На остальных вкладках разместим текстовые метки с описанием вкладки, взятом из текста на их заголовках.
Скомпилируем советник и запустим его на графике:
Ну что ж.. Время создания объектов достаточно продолжительное. Скоро нужно будет менять логику отображения объектов во время их массового создания. Этим мы вскоре займёмся. При выборе фиксированного размера заголовков вкладок и размера, подстраиваемого под ширину шрифта видно, что размеры вкладок отличаются. Выбор нужной вкладки и перестроение рядов вкладок работают верно. Объекты на вкладках доступны для взаимодействия с мышкой. Пока всё верно, а значит — можно продолжать развивать функционал элемента управления.
Что дальше
В следующей статье продолжим работу над WinForms-объектом TabControl.
Ниже прикреплены все файлы текущей версии библиотеки, файлы тестового советника и индикатора контроля событий графиков для MQL5. Их можно скачать и протестировать всё самостоятельно. При возникновении вопросов, замечаний и пожеланий вы можете озвучить их в комментариях к статье.
К содержанию
*Статьи этой серии:
DoEasy. Элементы управления (Часть 10): WinForms-объекты — оживляем интерфейс
DoEasy. Элементы управления (Часть 11): WinForms-объекты — группы, WinForms-объект CheckedListBox
DoEasy. Элементы управления (Часть 12): Базовый объект-список, WinForms-объекты ListBox и ButtonListBox
DoEasy. Элементы управления (Часть 13): Оптимизация взаимодействия WinForms-объектов с мышкой, начало разработки WinForms-объекта TabControl
DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl
This post will introduce you to the Tabbed Form control and its features, and we’ll see how to achieve tabbed navigation in your WinForms application.
We introduced the Telerik UI for WinForms RadTabbedForm in the R1 2019 version of the controls. I hope that by now you have played with it and seen how handy it can be in so many scenarios. In this blog post I will try to explain some of the scenarios the control handles and later go over the features
in more depth.
Where to Use Tabbed Form?
The idea to organize the different views of an application in separate tabs is not new. The Microsoft TabControl and our own RadPageView have been
around for quite some time. Although powerful, these controls serve a more limited purpose. Imagine, that you want to implement a tabbed UI which will serve as a top-level container of the entire application. The page view and MS tab control are not
suitable for this as they are bound to the form they were added to and the pages or tabs cannot be dragged and dropped. Actually, a dock control
can better serve you, yet if you want to achieve an end-user experience like what people are seeing in the modern browsers, it is still not good enough.
The RadTabbedForm is specifically built to handle this scenario. The application content can be organized and separated in different tabs where each tab is responsible for a different part of the application. The tabs are located inside
the form’s title bar just like in browsers (Chrome, Edge etc.) and they can be reordered or moved away to a separate window. This is extremely useful in big applications visualizing data. Instead of switching from one tab to another, the end user
could simply drag the tab out thus creating a new form. The tabs would then be in two separate windows and the user could easily view both contents simultaneously. RadTabbedForm offers great flexibility and freedom which will be appreciated
by the people using your application.
The screenshot below shows two tabs with the second tab being detached in a separate form. The first tab displays a RadPivotGrid and the second one shows a breakdown of the grouped and aggregated data behind the selected cell in the pivot from the first
tab.
Architecture and Features
The RadTabbedForm represents a form host of the RadTabbedFormControl. The form control is responsible for creating and manipulating the tabs in the title bar, the panels associated with each tab holding their content
and the drag-drop service. Basically, most of the functionality and API is defined inside the RadTabbedFormControl, which is exposed by the form’s TabbedFormControl property. The control is packed with many features
and its look can be completely customized by simply setting a couple of properties. The form is built on top of TPF consisting
of many light-weight visual elements. The Structure article in the documentation provides information on the more important
building blocks of the control.
Full Design Time support
Tabs can be added in the designer of Visual Studio. Each tab will create a panel where all the other controls can be added.
RadTabbedForm with the Fluent Theme in the Visual Studio Designer
Title Bar Support
It was really challenging to implement all the features inside the form’s non-client area. I am happy to say that we managed to achieve everything which was planned.
- Settings: The title bar’s height is not fixed, and it can be adjusted via the CaptionHeight property. The tabs height and the space between them is also adjustable. We have decided to leave no space between
the tabs, however, if your end users would need such you can increase the tab spacing. For example, with the following settings you can increase the height of the entire title bar and have the tabs displayed under the top-part where the form`s
icon and system buttons are located. This way you will have a bigger part of the non-client area empty which will allow the users to move the form easily.this
.ShowIcon =
true
;
this
.TabbedFormControl.ShowText =
true
;
this
.TabbedFormControl.CaptionHeight = 65;
this
.TabbedFormControl.TabHeight = 32;
this
.TabbedFormControl.TabWidth = 180;
this
.TabbedFormControl.TabSpacing = 12;
Default and Customized Tabbed Forms
- Standard Windows Title Bar Style: What does it mean? Basically, it means that we can paint the tabs inside the default Windows title bar as it is painted by the operating system. This will vary from one Windows version to another
and it will also depend on the chosen Windows theme. This behavior is controlled by the AllowAero property of the form. Setting the property to false would force our themes and the control will adjust the title bar to match the theme of the tabbed
control. In some situations, it might be necessary to match the standard Windows look so the AllowAero property has you covered:this
.AllowAero =
true
;
Standard Windows 10 Title Bar Style
Drag and Drop
The tabs can be reordered inside the title bar. It is also possible to drag them out to create a separate form. The tab content including all the added controls will be moved to the new form. This operation can be repeated numerous times over and the
tab be moved from one form to another.
Quick Actions
Various elements like buttons, check boxes, drop-down buttons etc. can be added before and/or after the tabs.
this
.TabbedFormControl.LeftItems.Add(
new
RadButtonElement { Text =
"button1"
});
//adds items before the tabs
this
.TabbedFormControl.RightItems.Add(
new
RadButtonElement { Text =
"button2"
});
//adds items after the tabs
These elements can be also added in the Visual Studio designer.
The LeftItems and RightItems collections are also exposed in the property grid of the designer and the elements can be edited in the collection editor.
Navigation
The tabbed control also has a built-in navigation. The navigation buttons will appear automatically, whenever the tabs cannot fit entirely in the available width. The tabs always have an equal width and it can be controlled with the TabWidth and MinimumTabWidth properties. The layout is smart enough and if the tabs count is increased to a point that they cannot fit in the title bar with their specified width, the widths of all tabs will be decreased equally. The layout
will decrease the widths of the tabs until it reaches the specified MinimumTabWidth value. At that moment the navigation buttons will become visible.
Pinned Items
Tabs can be pinned to the left or right side of the title bar. When unpinned, the tab will return to its last position.
this
.TabbedFormControl.Tabs[0].Item.IsPinned =
true
;
Context Menu
The tabbed control also has a built-in context menu which is fully localizable.
The items displayed in the menu can be customized in the RadTabbeFormControl.ContextMenuOpening event.
private
void
TabbedFormControl_ContextMenuOpening(
object
sender, RadTabbedFormControlItemConextMenuOpeningEventArgs e)
{
if
(e.TabItem.Text ==
"Tab 1"
)
{
//remove first item
e.ContextMenu.Items[0].Visibility = ElementVisibility.Collapsed;
}
else
if
(e.TabItem.Text ==
"Tab 2"
)
{
//disable the context menu
e.Cancel =
true
;
}
}
Getting Started
There are two ways to add a RadTabbedForm to your Visual Studio project:
- VSX extensions: Assuming that you have our Visual Studio extensions installed, you can create a new tabbed form by simply adding a new item to the project.
- RadFormConverter: Simply drag and drop the component from the Visual Studio tool box and convert the current form to a RadTabbedForm.
Once you have the form added to the project you can start building the UI. Tabs can be added via the RadTabbedFormControl’s smart tag. The smart tag also exposes the most important properties of the control. You can also check our Demo application and
the two RadTabbedForm examples. Please also don’t miss the documentation as it provides detailed information
on the features and API of the control.
Try It Out and Share Your Feedback
We’d love to hear what your thoughts are on the new control. Please leave a comment below or write in the forums. If you have an idea for the next great feature
you can also submit it on our feedback portal.
You can learn more about the Telerik UI for WinForms suite via the product page. It comes with a 30-day free trial, giving you some time to explore the toolkit and consider how it can help you with your current or upcoming WinForms development.