
Компоненты View и Controller для таблиц в парадигме MVC на MQL5: Контейнеры
Содержание
- Введение
- Класс-синглтон как менеджер общих данных
- Классы для организации автоповтора нажатия кнопок
- Доработка базовых классов
- Класс списка объектов
- Базовый класс графического элемента
- Доработка простых элементов управления
- Классы-контейнеры для размещения элементов управления
- Класс "Панель"
- Класс группы объектов
- Классы для создания полос прокрутки
- Классы ползунков полос прокрутки
- Класс горизонтальной полосы прокрутки
- Класс вертикальной полосы прокрутки
- Класс "Контейнер"
- Тестируем результат
- Заключение
Введение
В современных пользовательских интерфейсах довольно часто возникает необходимость компактно и удобно отображать большие объёмы различных данных. Для этих целей используются специальные элементы управления — контейнеры с поддержкой прокрутки содержимого. Такой подход позволяет размещать таблицы и другие графические элементы в ограниченном пространстве окна, обеспечивая пользователю быстрый и интуитивный доступ к информации.
В рамках разработки элемента управления TableView в парадигме MVC (Model-View-Controller) мы уже создали компонент Model — модель таблицы, и приступили к созданию компонентов View и Controller. В прошлой статье были созданы простые, но достаточно функциональные элементы управления. Из таких элементов и будут собираться сложные элементы управления. Сегодня напишем классы элементов управления Panel, GroupBox и Container — все три элемента являются контейнерами для размещения на них различных элементов управления.
- Элемент управления Panel — это панель, позволяющая размещать на себе иные элементы управления в любом количестве. При перемещении панели на новые координаты, все расположенные на ней элементы управления, также перемещаются совместно с панелью. Таким образом, панель является контейнером для расположенных на ней элементов управления. Но этот элемент не имеет полос прокрутки, позволяющих прокручивать содержимое контейнера в случае, если оно выходит за границы панели. Такое содержимое просто обрезается по границам контейнера.
- Элемент управления GroupBox — набор элементов, организованный в одну группу. Унаследован от панели и предоставляет возможность сгруппировать элементы по какому-либо их общему назначению, например — группа элементов RadioButton, где может быть выбран лишь один элемент из всей группы, а с остальных элементов группы при этом выделение снимается.
- Элемент управления Container — контейнер. Позволяет прикрепить к себе только один элемент управления. Если прикреплённый элемент выходит за пределы контейнера, то у контейнера появляются полосы прокрутки, позволяющие прокручивать содержимое контейнера. Чтобы разместить любое количество элементов управления в контейнере, необходимо в нём разместить панель, и уже к панели прикреплять необходимое количество элементов управления. Таким образом контейнер будет прокручивать панель, а уже она будет смещать своё содержимое вслед за прокруткой.
Таким образом нам потребуется создать, кроме трёх указанных основных элементов управления, классы для создания полос прокрутки — класс ползунка (Thumb) и класс полосы прокрутки (ScrollBar). Таких классов будет по два — для вертикальной и для горизонтальной полос прокрутки.
Если присмотреться к работе кнопок прокрутки, расположенных по краям полос прокрутки, то можно заметить, что при длительном удержании кнопки в нажатом положении, включается автоматическая прокрутка. Т.е. кнопка автоматически начинает отсылать события нажатия. Для такого поведения мы создадим ещё два вспомогательных класса — класс счётчика задержки и собственно класс автоповтора событий.
Класс счётчика задержки можно будет использовать для организации ожидания без замораживания работы программы, а класс автоповтора событий будет сделан так, чтобы мы могли указать для него, какое именно событие он должен отсылать. Это даст возможность использовать его не только для организации автоповтора нажатия кнопок, но и для каких-либо иных алгоритмов, требующих повтора событий через определённое время с определённой периодичностью.
На конечном этапе внутри универсального контейнера будут размещаться таблицы, а контейнер обеспечит прокрутку содержимого с помощью скроллбаров. Такой контейнер станет основой для построения сложных и гибких интерфейсов, позволяя не только работать с таблицами, но и использовать его в других компонентах — например, при создании многостраничного блокнота или других пользовательских элементов для клиентского терминала MetaTrader 5.
Стоит отметить каким образом устроена работа элементов управления. Каждый элемент управления наделён событийным функционалом (компонент Controller) и соответствующим образом реагирует на взаимодействие с курсором мышки.
При определённых действиях элемент, с которым происходит взаимодействие, отправляет на график событие, которое должен получить и обработать другой элемент управления. Но такие события получают все элементы. Нужно определить какой именно элемент является активным (над которым в данный момент находится курсор), и обрабатывать сообщения, полученные только от него. Т.е. при наведении курсора мышки на элемент управления он должен помечаться как активный, тогда остальные — неактивные элементы — обрабатываться не должны.
Чтобы организовать выбор активного элемента, необходимо сделать так, чтобы каждому элементу управления была доступна такая информация. Сделать это можно разными способами. Например, создать список, в который будут заноситься имена всех создаваемых элементов, искать в нём соответствие имени объекта, над которым сейчас находится курсор, и работать с найденным объектом в этом списке.
Такой вариант имеет право на существование, но влечёт за собой усложнение кода и работы с ним. Проще сделать один единственный объект, который глобально доступен в программе, и в него записывать имя активного элемента управления. Остальные элементы сразу же, без дополнительного поиска по какой-либо базе, будут видеть имя активного элемента и принимать решение обрабатывать ли поступающие сообщения, либо нет.
Таким общедоступным классом может быть класс-синглтон:
Класс-синглтон (от англ. singleton) — это шаблон проектирования (паттерн), который гарантирует существование только одного экземпляра данного класса в течение жизни программы и предоставляет глобальную точку доступа к этому экземпляру.
Для чего нужен синглтон
Синглтон используется, когда необходимо, чтобы у некоторого объекта был только один экземпляр, и этот экземпляр был доступен из любой части программы. Примеры: менеджер настроек, логгер, пул соединений с базой данных, диспетчер ресурсов и т.д.
Как работает синглтон
- Скрытый конструктор: Конструктор класса объявляется приватным или защищённым, чтобы предотвратить создание экземпляров извне.
- Статическая переменная: Внутри класса создаётся статическая переменная, хранящая единственный экземпляр класса.
- Статический метод доступа: Для получения экземпляра класса используется статический метод (например, Instance() или GetInstance() ), который создаёт объект при первом обращении и возвращает его при последующих вызовах.
Синглтон — это класс, который можно создать только один раз, и этот единственный экземпляр доступен глобально. Это удобно для управления общими ресурсами или состоянием приложения.
Давайте создадим такой класс.
Класс-синглтон как менеджер общих данных
В прошлой статье все коды библиотеки были расположены по адресу \MQL5\Indicators\Tables\Controls\. Здесь нас интересуют оба файла: Base.mqh и Control.mqh. Их сегодня будем дорабатывать.
Откроем файл Base.mqh и в блоке классов напишем такой код:
//+------------------------------------------------------------------+ //| Классы | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Класс-синглтон для общих флагов и событий графических элементов | //+------------------------------------------------------------------+ class CCommonManager { private: static CCommonManager *m_instance; // Экземпляр класса string m_element_name; // Имя активного элемента //--- Конструктор/деструктор CCommonManager(void) : m_element_name("") {} ~CCommonManager() {} public: //--- Метод для получения экземпляра Singleton static CCommonManager *GetInstance(void) { if(m_instance==NULL) m_instance=new CCommonManager(); return m_instance; } //--- Метод для уничтожения экземпляра Singleton static void DestroyInstance(void) { if(m_instance!=NULL) { delete m_instance; m_instance=NULL; } } //--- (1) Устанавливает, (2) возвращает имя активного текущего элемента void SetElementName(const string name) { this.m_element_name=name; } string ElementName(void) const { return this.m_element_name; } }; //--- Инициализация статической переменной экземпляра класса CCommonManager* CCommonManager::m_instance=NULL; //+------------------------------------------------------------------+ //| Базовый класс графических элементов | //+------------------------------------------------------------------+
Приватный конструктор класса и статический метод доступа к экземпляру класса гарантирует существование в приложении только одного экземпляра.
- Метод static CCommonManager* GetInstance(void) — возвращает указатель на единственный экземпляр класса, создавая его при первом обращении.
- Метод static void DestroyInstance(void) — уничтожает экземпляр класса и освобождает память.
- Метод void SetElementName(const string name) — устанавливает имя активного графического элемента.
- Метод string ElementName(void) const — возвращает имя активного графического элемента.
Теперь каждый из графических элементов может обращаться к экземпляру этого класса для чтения и записи имени активного элемента, и каждый из объектов будет видеть одну и ту же переменную с именем текущего активного элемента. Это гарантирует нам, что каждый из множества объектов будет читать и записывать данные в одну и ту же переменную.
Так как у нас не могут быть активными в текущий момент времени более одного элемента управления, то такой реализации менеджера активных элементов вполне достаточно и без функционала разграничения доступа (чтобы два и более элемента не могли записывать в переменную свои данные).
В последующем в этот класс менеджер данных можно будет добавить и другие данные, например, флаги разрешений для рабочего графика. На данный момент каждый из графических элементов при создании пытаются запоминать состояния флагов графика. Эти данные тоже можно будет перенести в этот класс в соответствующие переменные.
Классы для организации автоповтора нажатия кнопок
Выше говорили о создании функционала автоповтора отправки событий от кнопок полос прокрутки при длительном удержании кнопки. Такое поведение стандартно для большинства приложений ОС. Поэтому и здесь, считаю, нет причин не сделать то же самое. При нажатии на кнопку и её удержании, сначала запускается счётчик времени удержания кнопки (обычно этот период составляет 350 — 500 мсек). Далее, если кнопка не была отпущена до истечения времени удержания, запускается второй счётчик — счётчик интервала отправки событий нажатия кнопки. И такие события отправляются с периодичностью около 100 мсек до тех пор, пока кнопка не будет отпущена.
Для реализации такого поведения напишем два вспомогательных класса — класс счётчика миллисекунд и класс автоповтора отправки событий.
Продолжим писать код в том же файле Base.mqh:
//+------------------------------------------------------------------+ //| Класс счётчика миллисекунд | //+------------------------------------------------------------------+ class CCounter : public CBaseObj { private: bool m_launched; // Флаг запущенного отсчёта //--- Запускает отсчёт void Run(const uint delay) { //--- Если отсчёт уже запущен - уходим if(this.m_launched) return; //--- Если передано ненулевое значение задержки - устанавливаем новое значение if(delay!=0) this.m_delay=delay; //--- Запоминаем время запуска и устанавливаем флаг, что отсчёт уже запущен this.m_start=::GetTickCount64(); this.m_launched=true; } protected: ulong m_start; // Время начала отсчёта uint m_delay; // Задержка public: //--- (1) Устанавливает задержку, запускает отсчёт с (2) установленной, (3) указанной задержкой void SetDelay(const uint delay) { this.m_delay=delay; } void Start(void) { this.Run(0); } void Start(const uint delay) { this.Run(delay); } //--- Возвращает флаг окончания отсчёта bool IsDone(void) { //--- Если отсчёт не запущен - возвращаем false if(!this.m_launched) return false; //--- Если прошло миллисекунд больше, чем время ожидания if(::GetTickCount64()-this.m_start>this.m_delay) { //--- сбрасываем флаг запущенного отсчёта и возвращаем true this.m_launched=false; return true; } //--- Заданное время ещё не прошло return false; } //--- Виртуальные методы (1) сохранения в файл, (2) загрузки из файла, (3) тип объекта virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_COUNTER); } //--- Конструктор/деструктор CCounter(void) : m_start(0), m_delay(0), m_launched(false) {} ~CCounter(void) {} }; //+------------------------------------------------------------------+ //| CCounter::Сохранение в файл | //+------------------------------------------------------------------+ bool CCounter::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CBaseObj::Save(file_handle)) return false; //--- Сохраняем значение задержки if(::FileWriteInteger(file_handle,this.m_delay,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CCounter::Загрузка из файла | //+------------------------------------------------------------------+ bool CCounter::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CBaseObj::Load(file_handle)) return false; //--- Загружаем значение задержки this.m_delay=::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
Класс-счётчик миллисекунд предназначен для отслеживания истечения заданного временного интервала (задержки) в миллисекундах. Унаследован от базового класса CBaseObj и может использоваться для реализации таймеров, задержек и контроля времени выполнения различных операций в приложениях на MQL5.
- Метод void SetDelay(const uint delay) — устанавливает значение задержки (в миллисекундах).
- Метод void Start(const uint delay) — запускает отсчёт с новой задержкой.
- Метод bool IsDone(void) — возвращает true, если отсчёт завершён, иначе false.
- Метод virtual bool Save(const int file_handle) — виртуальный метод для сохранения состояния в файл.
- Метод virtual bool Load(const int file_handle) — виртуальный метод для загрузки состояния из файла.
- Метод virtual int Type(void) const — возвращает тип объекта для идентификации в системе.
На базе данного класса создадим класс автоповтора событий:
//+------------------------------------------------------------------+ //| Класс автоповтора событий | //+------------------------------------------------------------------+ class CAutoRepeat : public CBaseObj { private: CCounter m_delay_counter; // Счетчик для задержки перед автоповтором CCounter m_repeat_counter; // Счетчик для периодической отправки событий long m_chart_id; // График для отправки пользовательского события bool m_button_pressed; // Флаг, указывающий нажата ли кнопка bool m_auto_repeat_started; // Флаг, указывающий начался ли автоповтор uint m_delay_before_repeat; // Задержка перед началом автоповтора (мс) uint m_repeat_interval; // Периодичность отправки событий (мс) ushort m_event_id; // Идентификатор пользовательского события long m_event_lparam; // long-параметр пользовательского события double m_event_dparam; // double-параметр пользовательского события string m_event_sparam; // string-параметр пользовательского события //--- Отправка пользовательского события void SendEvent() { ::EventChartCustom((this.m_chart_id<=0 ? ::ChartID() : this.m_chart_id), this.m_event_id, this.m_event_lparam, this.m_event_dparam, this.m_event_sparam); } public: //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_AUTOREPEAT_CONTROL); } //--- Конструкторы CAutoRepeat(void) : m_button_pressed(false), m_auto_repeat_started(false), m_delay_before_repeat(350), m_repeat_interval(100), m_event_id(0), m_event_lparam(0), m_event_dparam(0), m_event_sparam(""), m_chart_id(::ChartID()) {} CAutoRepeat(long chart_id, int delay_before_repeat=350, int repeat_interval=100, ushort event_id=0, long event_lparam=0, double event_dparam=0, string event_sparam="") : m_button_pressed(false), m_auto_repeat_started(false), m_delay_before_repeat(delay_before_repeat), m_repeat_interval(repeat_interval), m_event_id(event_id), m_event_lparam(event_lparam), m_event_dparam(event_dparam), m_event_sparam(event_sparam), m_chart_id(chart_id) {} //--- Установка идентификатора графика void SetChartID(const long chart_id) { this.m_chart_id=chart_id; } void SetDelay(const uint delay) { this.m_delay_before_repeat=delay; } void SetInterval(const uint interval) { this.m_repeat_interval=interval; } //--- Установка идентификатора и параметров пользовательского события void SetEvent(ushort event_id, long event_lparam, double event_dparam, string event_sparam) { this.m_event_id=event_id; this.m_event_lparam=event_lparam; this.m_event_dparam=event_dparam; this.m_event_sparam=event_sparam; } //--- Возврат флагов bool ButtonPressedFlag(void) const { return this.m_button_pressed; } bool AutorepeatStartedFlag(void) const { return this.m_auto_repeat_started;} uint Delay(void) const { return this.m_delay_before_repeat;} uint Interval(void) const { return this.m_repeat_interval; } //--- Обработка нажатия кнопки (запуск автоповтора) void OnButtonPress(void) { if(this.m_button_pressed) return; this.m_button_pressed=true; this.m_auto_repeat_started=false; this.m_delay_counter.Start(this.m_delay_before_repeat); // Запускаем счётчик ожидания } //--- Обработка отпускания кнопки (остановка автоповтора) void OnButtonRelease(void) { this.m_button_pressed=false; this.m_auto_repeat_started=false; } //--- Метод выполнения автоповтора (запускается в таймере) void Process(void) { //--- Если кнопка удерживается if(this.m_button_pressed) { //--- Проверяем, истекла ли задержка перед началом автоповтора if(!this.m_auto_repeat_started && this.m_delay_counter.IsDone()) { this.m_auto_repeat_started=true; this.m_repeat_counter.Start(this.m_repeat_interval); // Запускаем счетчик автоповтора } //--- Если автоповтор начался, проверяем периодичность отправки событий if(this.m_auto_repeat_started && this.m_repeat_counter.IsDone()) { //--- Отправляем событие и перезапускаем счетчик this.SendEvent(); this.m_repeat_counter.Start(this.m_repeat_interval); } } } };
Класс позволяет автоматически с заданной периодичностью отправлять пользовательские события, пока кнопка удерживается нажатой. Это обеспечивает привычное для пользователя поведение интерфейса (как в стандартных скроллбарах ОС).
- Метод OnButtonPress() — вызывается при нажатии кнопки; запускает отсчёт задержки перед автоповтором.
- Метод OnButtonRelease() — вызывается при отпускании кнопки; останавливает автоповтор.
- Метод Process() — основной метод, который должен вызываться в таймере. Обеспечивает отправку событий с нужной периодичностью, если кнопка удерживается.
- Метод SetEvent(...) — установка параметров пользовательского события.
- Методы SetDelay(...), SetInterval(...) — установка задержки и интервала автоповтора.
Объект класса автоповтора объявим в базовом классе холста графических элементов CCanvasBase. Таким образом, в любом объекте графических элементов будет возможность использовать автоповтор событий — достаточно будет установить параметры задержки и интервала, и запускать автоповтор в требуемых ситуациях.
Доработка базовых классов
Над библиотекой была проведена большая работа над ошибками и устранением недочётов. Доработки коснулись практически каждого класса. Здесь не будем описывать каждый шаг проделанной работы. Но основные моменты, конечно же, будут озвучены.
В файле Base.mqh объявим все классы, которые сегодня будут сделаны:
//+------------------------------------------------------------------+ //| Base.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include <Canvas\Canvas.mqh> // Класс СБ CCanvas #include <Arrays\List.mqh> // Класс СБ CList //--- Форвард-декларация классов элементов управления class CCounter; // Класс счётчика задержки class CAutoRepeat; // Класс автоповтора событий class CImagePainter; // Класс рисования изображений class CLabel; // Класс текстовой метки class CButton; // Класс простой кнопки class CButtonTriggered; // Класс двухпозиционной кнопки class CButtonArrowUp; // Класс кнопки со стрелкой вверх class CButtonArrowDown; // Класс кнопки со стрелкой вниз class CButtonArrowLeft; // Класс кнопки со стрелкой влево class CButtonArrowRight; // Класс кнопки со стрелкой вправо class CCheckBox; // Класс элемента управления CheckBox class CRadioButton; // Класс элемента управления RadioButton class CScrollBarThumbH; // Класс ползунка горизонтальной полосы прокрутки class CScrollBarThumbV; // Класс ползунка вертикальной полосы прокрутки class CScrollBarH; // Класс горизонтальной полосы прокрутки class CScrollBarV; // Класс вертикальной полосы прокрутки class CPanel; // Класс элемента управления Panel class CGroupBox; // Класс элемента управления GroupBox class CContainer; // Класс элемента управления Container
Такая форвард-декларация классов нужна для безошибочной компиляции включаемых файлов Base.mqh и Controls.mqh, так как обращение к этим классам выполнено ещё до их фактического объявления в файлах.
В перечисление типов графических элементов добавим новые типы и укажем диапазон констант типов объектов, которые могут участвовать в интерактивном взаимодействии с пользователем:
//+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_TYPE // Перечисление типов графических элементов { ELEMENT_TYPE_BASE = 0x10000, // Базовый объект графических элементов ELEMENT_TYPE_COLOR, // Объект цвета ELEMENT_TYPE_COLORS_ELEMENT, // Объект цветов элемента графического объекта ELEMENT_TYPE_RECTANGLE_AREA, // Прямоугольная область элемента ELEMENT_TYPE_IMAGE_PAINTER, // Объект для рисования изображений ELEMENT_TYPE_COUNTER, // Объект счётчика ELEMENT_TYPE_AUTOREPEAT_CONTROL, // Объект автоповтора событий ELEMENT_TYPE_CANVAS_BASE, // Базовый объект холста графических элементов ELEMENT_TYPE_ELEMENT_BASE, // Базовый объект графических элементов ELEMENT_TYPE_LABEL, // Текстовая метка ELEMENT_TYPE_BUTTON, // Простая кнопка ELEMENT_TYPE_BUTTON_TRIGGERED, // Двухпозиционная кнопка ELEMENT_TYPE_BUTTON_ARROW_UP, // Кнопка со стрелкой вверх ELEMENT_TYPE_BUTTON_ARROW_DOWN, // Кнопка со стрелкой вниз ELEMENT_TYPE_BUTTON_ARROW_LEFT, // Кнопка со стрелкой влево ELEMENT_TYPE_BUTTON_ARROW_RIGHT, // Кнопка со стрелкой вправо ELEMENT_TYPE_CHECKBOX, // Элемент управления CheckBox ELEMENT_TYPE_RADIOBUTTON, // Элемент управления RadioButton ELEMENT_TYPE_SCROLLBAR_THUMB_H, // Ползунок горизонтальной полосы прокрутки ELEMENT_TYPE_SCROLLBAR_THUMB_V, // Ползунок вертикальной полосы прокрутки ELEMENT_TYPE_SCROLLBAR_H, // Элемент управления ScrollBarHorisontal ELEMENT_TYPE_SCROLLBAR_V, // Элемент управления ScrollBarVertical ELEMENT_TYPE_PANEL, // Элемент управления Panel ELEMENT_TYPE_GROUPBOX, // Элемент управления GroupBox ELEMENT_TYPE_CONTAINER, // Элемент управления Container }; #define ACTIVE_ELEMENT_MIN ELEMENT_TYPE_LABEL // Минимальное значение списка активных элементов #define ACTIVE_ELEMENT_MAX ELEMENT_TYPE_SCROLLBAR_V // Максимальное значение списка активных элементов
При взаимодействии с курсором мышки каждый графический элемент в принципе в состоянии обрабатывать поступающие сообщения о событиях. Но не каждый элемент должен это делать. Т.е. необходимо проверять тип элемента и на его основании принимать решение — обрабатывает ли этот элемент события, либо нет. Если пойти по пути проверки типов объектов, то получим длинный список необрабатываемых элементов в условии. Это не удобно. Проще добавить ещё одно свойство-флаг, в котором будет указано, активный ли это элемент для взаимодействия, либо он статичен. Тогда мы сможем проверять лишь это свойство для принятия решения обрабатывать ли событие, или нет. Здесь мы указали начальное и конечное значения констант типов графических элементов. При принятии решения по обработке события, достаточно лишь проверить входит ли тип элемента в этот диапазон значений, и на этом основании уже принимать решение.
Добавим перечисление свойств, по которым можно сортировать и искать базовые объекты (CBaseObj):
enum ENUM_BASE_COMPARE_BY // Сравниваемые свойства базовых объектов { BASE_SORT_BY_ID = 0, // Сравнение базовых объектов по идентификатору BASE_SORT_BY_NAME, // Сравнение базовых объектов по имени BASE_SORT_BY_X, // Сравнение базовых объектов по координате X BASE_SORT_BY_Y, // Сравнение базовых объектов по координате Y BASE_SORT_BY_WIDTH, // Сравнение базовых объектов по ширине BASE_SORT_BY_HEIGHT, // Сравнение базовых объектов по высоте BASE_SORT_BY_ZORDER, // Сравнение по Z-order объектов };
Теперь все объекты, унаследованные от базового, можно будет сортировать по указанным в перечислении свойствам в случае, если такие свойства есть у объекта, что добавит больше гибкости при создании новых классов-наследников CBaseObj.
В функции, возвращающей тип элемента как строку, дополним вывод букв "V" и "H" на читаемые "Vertical" и "Horizontal":
//+------------------------------------------------------------------+ //| Возвращает тип элемента как строку | //+------------------------------------------------------------------+ string ElementDescription(const ENUM_ELEMENT_TYPE type) { string array[]; int total=StringSplit(EnumToString(type),StringGetCharacter("_",0),array); if(array[array.Size()-1]=="V") array[array.Size()-1]="Vertical"; if(array[array.Size()-1]=="H") array[array.Size()-1]="Horisontal"; string result=""; for(int i=2;i<total;i++) { array[i]+=" "; array[i].Lower(); array[i].SetChar(0,ushort(array[i].GetChar(0)-0x20)); result+=array[i]; } result.TrimLeft(); result.TrimRight(); return result; }
Это сделает описания элементов более читаемыми.
При создании новых элементов и добавлении их в список прикреплённых к контейнеру, необходимо будет создавать имена объектов. Сделаем функцию, которая будет возвращать короткие аббревиатуры типов элементов, по которым можно будет создать элемент, а далее понимать по этой аббревиатуре в имени объекта, что это за элемент:
//+------------------------------------------------------------------+ //| Возвращает короткое имя элемента по типу | //+------------------------------------------------------------------+ string ElementShortName(const ENUM_ELEMENT_TYPE type) { switch(type) { case ELEMENT_TYPE_ELEMENT_BASE : return "BASE"; // Базовый объект графических элементов case ELEMENT_TYPE_LABEL : return "LBL"; // Текстовая метка case ELEMENT_TYPE_BUTTON : return "SBTN"; // Простая кнопка case ELEMENT_TYPE_BUTTON_TRIGGERED : return "TBTN"; // Двухпозиционная кнопка case ELEMENT_TYPE_BUTTON_ARROW_UP : return "BTARU"; // Кнопка со стрелкой вверх case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return "BTARD"; // Кнопка со стрелкой вниз case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return "BTARL"; // Кнопка со стрелкой влево case ELEMENT_TYPE_BUTTON_ARROW_RIGHT: return "BTARR"; // Кнопка со стрелкой вправо case ELEMENT_TYPE_CHECKBOX : return "CHKB"; // Элемент управления CheckBox case ELEMENT_TYPE_RADIOBUTTON : return "RBTN"; // Элемент управления RadioButton case ELEMENT_TYPE_SCROLLBAR_THUMB_H : return "THMBH"; // Ползунок горизонтальной полосы прокрутки case ELEMENT_TYPE_SCROLLBAR_THUMB_V : return "THMBV"; // Ползунок вертикальной полосы прокрутки case ELEMENT_TYPE_SCROLLBAR_H : return "SCBH"; // Элемент управления ScrollBarHorisontal case ELEMENT_TYPE_SCROLLBAR_V : return "SCBV"; // Элемент управления ScrollBarVertical case ELEMENT_TYPE_PANEL : return "PNL"; // Элемент управления Panel case ELEMENT_TYPE_GROUPBOX : return "GRBX"; // Элемент управления GroupBox case ELEMENT_TYPE_CONTAINER : return "CNTR"; // Элемент управления Container default : return "Unknown"; // Unknown } }
При прикреплении элементов к контейнеру, их имена будут создаваться с учётом иерархии объектов: "контейнер -- прикреплённый элемент к контейнеру --- прикреплённый элемент к прикреплённому элементу", и т.д.
Разделителями между именами элементов в строке имени будут знаки подчёркивания ("_"). Мы можем из полного имени создать список имён всей иерархии объектов. Напишем для этого функцию:
//+------------------------------------------------------------------+ //| Возвращает массив имён иерархии элементов | //+------------------------------------------------------------------+ int GetElementNames(string value, string sep, string &array[]) { if(value=="" || value==NULL) { PrintFormat("%s: Error. Empty string passed"); return 0; } ResetLastError(); int res=StringSplit(value, StringGetCharacter(sep,0),array); if(res==WRONG_VALUE) { PrintFormat("%s: StringSplit() failed. Error %d",__FUNCTION__, GetLastError()); return WRONG_VALUE; } return res; }
Функция возвращает количество объектов в иерархии и заполняет массив имён всех элементов.
В классе прямоугольной области CBound напишем метод сравнения двух объектов:
//+------------------------------------------------------------------+ //| CBound::Сравнение двух объектов | //+------------------------------------------------------------------+ int CBound::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; const CBound *obj=node; switch(mode) { case BASE_SORT_BY_NAME : return(this.Name() >obj.Name() ? 1 : this.Name() <obj.Name() ? -1 : 0); case BASE_SORT_BY_X : return(this.X() >obj.X() ? 1 : this.X() <obj.X() ? -1 : 0); case BASE_SORT_BY_Y : return(this.Y() >obj.Y() ? 1 : this.Y() <obj.Y() ? -1 : 0); case BASE_SORT_BY_WIDTH : return(this.Width() >obj.Width() ? 1 : this.Width() <obj.Width() ? -1 : 0); case BASE_SORT_BY_HEIGHT: return(this.Height() >obj.Height() ? 1 : this.Height() <obj.Height() ? -1 : 0); default : return(this.ID() >obj.ID() ? 1 : this.ID() <obj.ID() ? -1 : 0); } }
Ранее сравнение проводилось при помощи одноимённого метода родительского класса, что давало возможность сравнения только по двум свойствам: имени и идентификатору объекта.
Львиная доля доработок коснулась базового класса объекта холста графических элементов CCanvasBase, так как именно в нём аккумулированы основные свойства всех графических элементов.
В защищённой секции класса объявим новые переменные и три метода для работы с менеджером общих ресурсов:
//+------------------------------------------------------------------+ //| Базовый класс холста графических элементов | //+------------------------------------------------------------------+ class CCanvasBase : public CBaseObj { private: bool m_chart_mouse_wheel_flag; // Флаг отправки сообщений о прокрутке колёсика мышки bool m_chart_mouse_move_flag; // Флаг отправки сообщений о перемещениях курсора мышки bool m_chart_object_create_flag; // Флаг отправки сообщений о событии создания графического объекта bool m_chart_mouse_scroll_flag; // Флаг прокрутки графика левой кнопкой и колёсиком мышки bool m_chart_context_menu_flag; // Флаг доступа к контекстному меню по нажатию правой клавиши мышки bool m_chart_crosshair_tool_flag; // Флаг доступа к инструменту "Перекрестие" по нажатию средней клавиши мышки bool m_flags_state; // Состояние флагов прокрутки графика колёсиком, контекстного меню и перекрестия //--- Установка запретов для графика (прокрутка колёсиком, контекстное меню и перекрестие) void SetFlags(const bool flag); protected: CCanvas m_background; // Канвас для рисования фона CCanvas m_foreground; // Канвас для рисования переднего плана CBound m_bound; // Границы объекта CCanvasBase *m_container; // Родительский объект-контейнер CColorElement m_color_background; // Объект управления цветом фона CColorElement m_color_foreground; // Объект управления цветом переднего плана CColorElement m_color_border; // Объект управления цветом рамки CColorElement m_color_background_act; // Объект управления цветом фона активированного элемента CColorElement m_color_foreground_act; // Объект управления цветом переднего плана активированного элемента CColorElement m_color_border_act; // Объект управления цветом рамки активированного элемента CAutoRepeat m_autorepeat; // Объект управления автоповторами событий ENUM_ELEMENT_STATE m_state; // Состояние элемента (напр., кнопки (вкл/выкл)) long m_chart_id; // Идентификатор графика int m_wnd; // Номер подокна графика int m_wnd_y; // Смещение координаты Y курсора в подокне int m_obj_x; // Координата X графического объекта int m_obj_y; // Координата Y графического объекта uchar m_alpha_bg; // Прозрачность фона uchar m_alpha_fg; // Прозрачность переднего плана uint m_border_width_lt; // Ширина рамки слева uint m_border_width_rt; // Ширина рамки справа uint m_border_width_up; // Ширина рамки сверху uint m_border_width_dn; // Ширина рамки снизу string m_program_name; // Имя программы bool m_hidden; // Флаг скрытого объекта bool m_blocked; // Флаг заблокированного элемента bool m_movable; // Флаг перемещаемого элемента bool m_focused; // Флаг элемента в фокусе bool m_main; // Флаг главного объекта bool m_autorepeat_flag; // Флаг автоповтора отправки событий bool m_scroll_flag; // Флаг прокрутки содержимого при помощи скроллбаров bool m_trim_flag; // Флаг обрезки элемента по границам контейнера int m_cursor_delta_x; // Дистанция от курсора до левого края элемента int m_cursor_delta_y; // Дистанция от курсора до верхнего края элемента int m_z_order; // Z-ордер графического объекта //--- (1) Устанавливает, возвращает (2) имя, (3) флаг активного элемента void SetActiveElementName(const string name) { CCommonManager::GetInstance().SetElementName(name); } string ActiveElementName(void) const { return CCommonManager::GetInstance().ElementName(); } bool IsCurrentActiveElement(void) const { return this.ActiveElementName()==this.NameFG(); } //--- Возврат смещения начальных координат рисования на холсте относительно канваса и координат объекта int CanvasOffsetX(void) const { return(this.ObjectX()-this.X()); } int CanvasOffsetY(void) const { return(this.ObjectY()-this.Y()); } //--- Возвращает скорректированную координату точки на холсте с учётом смещения холста относительно объекта int AdjX(const int x) const { return(x-this.CanvasOffsetX()); } int AdjY(const int y) const { return(y-this.CanvasOffsetY()); } //--- Возвращает скорректированный идентификатор графика long CorrectChartID(const long chart_id) const { return(chart_id!=0 ? chart_id : ::ChartID()); } public:
- CAutoRepeat m_autorepeat — объект автоповтора событий; любой из графических элементов может иметь функционал, предоставляемый классом этого объекта.
- uint m_border_width_lt — ширина рамки слева; рамка является границей видимой области контейнера, и отступ видимой области от края элемента может быть с разных сторон разного размера.
- uint m_border_width_rt — ширина рамки справа.
- uint m_border_width_up — ширина рамки сверху.
- uint m_border_width_dn — ширина рамки снизу.
- bool m_movable — флаг перемещаемости объекта; например, кнопка — неперемещаемый элемент, ползунок скроллбара — перемещаемый, и т.п.
- bool m_main — флаг главного элемента; главным элементом является самый первый в иерархии связанных объектов, например, панель, на которой размещены иные элементы управления; обычно это — объект-форма.
- bool m_autorepeat_flag — флаг использования элементом автоповтора событий.
- bool m_scroll_flag — флаг прокрутки элемента полосами прокрутки.
- bool m_trim_flag — флаг обрезки элемента по краям видимой области контейнера; например, полосы прокрутки находятся за пределами видимой области контейнера, но не обрезаются по её краям.
- int m_cursor_delta_x — вспомогательная переменная, хранящая дистанцию курсора от левой границы элемента.
- int m_cursor_delta_y — вспомогательная переменная, хранящая дистанцию курсора от верхней границы элемента.
- int m_z_order — приоритет графического объекта на получение события нажатия мышки на графике; при наложении объектов друг на друга событие CHARTEVENT_CLICK получит только один объект, чей приоритет выше остальных.
- Метод void SetActiveElementName(const string name) — устанавливает в менеджер общих данных имя текущего активного элемента.
- Метод string ActiveElementName(void) — возвращает имя текущего активного элемента.
- Метод bool IsCurrentActiveElement(void) — возвращает флаг того, что данный объект сейчас является текущим активным.
В защищённой же секции класса добавим обработчики перемещения курсора мышки и изменения элемента управления:
//--- Обработчики событий (1) наведения курсора (Focus), (2) нажатий кнопок мышки (Press), (3) перемещения курсора (Move), //--- (4) прокрутки колёсика (Wheel), (5) ухода из фокуса (Release), (6) создания графического объекта (Create). Переопределяются в наследниках. virtual void OnFocusEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnPressEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnMoveEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnReleaseEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnCreateEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam) { return; } // обработчик здесь отключен //--- Обработчики пользовательских событий элемента при наведении курсора, щелчке, прокрутке колёсика в области объекта и его изменения virtual void MouseMoveHandler(const int id, const long lparam, const double dparam, const string sparam) { return; } // обработчик здесь отключен virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam) { return; } // обработчик здесь отключен virtual void MouseWheelHandler(const int id, const long lparam, const double dparam, const string sparam) { return; } // обработчик здесь отключен virtual void ObjectChangeHandler(const int id, const long lparam, const double dparam, const string sparam) { return; } // обработчик здесь отключен
При перемещении курсора над объектом такие события необходимо обрабатывать, и некоторые элементы управления в последующем будут иметь возможность изменения размеров мышкой. Обработчики таких событий мы здесь и объявили.
В публичной секции класса добавим методы для работы с некоторыми переменными класса:
public: //--- Возвращает указатель на (1) контейнер, (2) объект класса автоповтора событий CCanvasBase *GetContainer(void) const { return this.m_container; } CAutoRepeat *GetAutorepeatObj(void) { return &this.m_autorepeat; }
...
//--- (1) Устанавливает, (2) возвращает z-ордер bool ObjectSetZOrder(const int value); int ObjectZOrder(void) const { return this.m_z_order; } //--- Возвращает (1) принадлежность объекта программе, флаг (2) скрытого, (3) заблокированного, //--- (4) перемещаемого, (5) главного элемента, (6) в фокусе, (7) имя графического объекта (фон, текст) bool IsBelongsToThis(const string name) const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);} bool IsHidden(void) const { return this.m_hidden; } bool IsBlocked(void) const { return this.m_blocked; } bool IsMovable(void) const { return this.m_movable; } bool IsMain(void) const { return this.m_main; } bool IsFocused(void) const { return this.m_focused; } string NameBG(void) const { return this.m_background.ChartObjectName(); } string NameFG(void) const { return this.m_foreground.ChartObjectName(); }
...
//--- (1) Возвращает, (2) устанавливает ширину рамки слева
uint BorderWidthLeft(void) const { return this.m_border_width_lt; }
void SetBorderWidthLeft(const uint width) { this.m_border_width_lt=width; }
//--- (1) Возвращает, (2) устанавливает ширину рамки справа
uint BorderWidthRight(void) const { return this.m_border_width_rt; }
void SetBorderWidthRight(const uint width) { this.m_border_width_rt=width; }
//--- (1) Возвращает, (2) устанавливает ширину рамки сверху
uint BorderWidthTop(void) const { return this.m_border_width_up; }
void SetBorderWidthTop(const uint width) { this.m_border_width_up=width; }
//--- (1) Возвращает, (2) устанавливает ширину рамки снизу
uint BorderWidthBottom(void) const { return this.m_border_width_dn; }
void SetBorderWidthBottom(const uint width) { this.m_border_width_dn=width; }
//--- Устанавливает одинаковую ширину рамки со всех сторон
void SetBorderWidth(const uint width)
{
this.m_border_width_lt=this.m_border_width_rt=this.m_border_width_up=this.m_border_width_dn=width;
}
//--- Устанавливает ширину рамки
void SetBorderWidth(const uint left,const uint right,const uint top,const uint bottom)
{
this.m_border_width_lt=left;
this.m_border_width_rt=right;
this.m_border_width_up=top;
this.m_border_width_dn=bottom;
}
...
Некоторые методы необходимо сделать виртуальными, так как у разных элементов они должны работать по-разному
//--- Устанавливает объекту флаг (1) перемещаемости, (2) главного объекта void SetMovable(const bool flag) { this.m_movable=flag; } void SetAsMain(void) { this.m_main=true; } //--- Ограничивает графический объект по размерам контейнера virtual bool ObjectTrim(void); //--- Изменяет размеры объекта virtual bool ResizeW(const int w); virtual bool ResizeH(const int h); virtual bool Resize(const int w,const int h); //--- Устанавливает объекту новую координату (1) X, (2) Y, (3) XY virtual bool MoveX(const int x); virtual bool MoveY(const int y); virtual bool Move(const int x,const int y); //--- Смещает объект по оси (1) X, (2) Y, (3) XY на указанное смещение virtual bool ShiftX(const int dx); virtual bool ShiftY(const int dy); virtual bool Shift(const int dx,const int dy);
...
//--- Обработчик событий virtual void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- (1) Таймер, (2) обработчик события таймера virtual void OnTimer() { this.TimerEventHandler(); } virtual void TimerEventHandler(void) { return; }
В конструкторах класса все новые переменные инициализированы значениями по умолчанию:
//--- Конструкторы/деструктор CCanvasBase(void) : m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_chart_id(::ChartID()), m_wnd(0), m_alpha_bg(0), m_alpha_fg(255), m_hidden(false), m_blocked(false), m_focused(false), m_movable(false), m_main(false), m_autorepeat_flag(false), m_trim_flag(true), m_scroll_flag(false), m_border_width_lt(0), m_border_width_rt(0), m_border_width_up(0), m_border_width_dn(0), m_z_order(0), m_state(0), m_wnd_y(0), m_cursor_delta_x(0), m_cursor_delta_y(0) { this.Init(); } CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h); ~CCanvasBase(void); }; //+------------------------------------------------------------------+ //| CCanvasBase::Конструктор | //+------------------------------------------------------------------+ CCanvasBase::CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_wnd(wnd<0 ? 0 : wnd), m_alpha_bg(0), m_alpha_fg(255), m_hidden(false), m_blocked(false), m_focused(false), m_movable(false), m_main(false), m_autorepeat_flag(false), m_trim_flag(true), m_scroll_flag(false), m_border_width_lt(0), m_border_width_rt(0), m_border_width_up(0), m_border_width_dn(0), m_z_order(0), m_state(0), m_cursor_delta_x(0), m_cursor_delta_y(0) { ...
Добавим реализацию виртуального метода сравнения Compare:
//+------------------------------------------------------------------+ //| CCanvasBase::Сравнение двух объектов | //+------------------------------------------------------------------+ int CCanvasBase::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; const CCanvasBase *obj=node; switch(mode) { case BASE_SORT_BY_NAME : return(this.Name() >obj.Name() ? 1 : this.Name() <obj.Name() ? -1 : 0); case BASE_SORT_BY_X : return(this.X() >obj.X() ? 1 : this.X() <obj.X() ? -1 : 0); case BASE_SORT_BY_Y : return(this.Y() >obj.Y() ? 1 : this.Y() <obj.Y() ? -1 : 0); case BASE_SORT_BY_WIDTH : return(this.Width() >obj.Width() ? 1 : this.Width() <obj.Width() ? -1 : 0); case BASE_SORT_BY_HEIGHT: return(this.Height() >obj.Height() ? 1 : this.Height() <obj.Height() ? -1 : 0); case BASE_SORT_BY_ZORDER: return(this.ObjectZOrder() >obj.ObjectZOrder() ? 1 : this.ObjectZOrder() <obj.ObjectZOrder() ? -1 : 0); default : return(this.ID() >obj.ID() ? 1 : this.ID() <obj.ID() ? -1 : 0); } }
Доработаем метод, подрезающий графический объект по контуру контейнера:
//+------------------------------------------------------------------+ //| CCanvasBase::Подрезает графический объект по контуру контейнера | //+------------------------------------------------------------------+ bool CCanvasBase::ObjectTrim() { //--- Проверяем флаг разрешения обрезки элемента и, //--- если элемент не должен обрезаться по границам контейнера - возвращаем false if(!this.m_trim_flag) return false; //--- Получаем границы контейнера int container_left = this.ContainerLimitLeft(); int container_right = this.ContainerLimitRight(); int container_top = this.ContainerLimitTop(); int container_bottom = this.ContainerLimitBottom(); //--- Получаем текущие границы объекта int object_left = this.X(); int object_right = this.Right(); int object_top = this.Y(); int object_bottom = this.Bottom(); //--- Проверяем, полностью ли объект выходит за пределы контейнера и, если да - скрываем его if(object_right <= container_left || object_left >= container_right || object_bottom <= container_top || object_top >= container_bottom) { this.Hide(true); if(this.ObjectResize(this.Width(),this.Height())) this.BoundResize(this.Width(),this.Height()); return false; } //--- Объект полностью или частично находится внутри видимой области контейнера else { //--- Если элемент полностью внутри контейнера if(object_right<=container_right && object_left>=container_left && object_bottom<=container_bottom && object_top>=container_top) { //--- Если ширина или высота графического объекта не совпадает с шириной или высотой элемента, //--- модифицируем графический объект по размерам элемента и возвращаем true if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height()) { if(this.ObjectResize(this.Width(),this.Height())) return true; } } //--- Если элемент частично находится в видимой области контейнера else { //--- Если элемент по вертикали находится в видимой области контейнера if(object_bottom<=container_bottom && object_top>=container_top) { //--- Если высота графического объекта не совпадает с высотой элемента, //--- модифицируем графический объект по высоте элемента if(this.ObjectHeight()!=this.Height()) this.ObjectResizeH(this.Height()); } else { //--- Если элемент по горизонтали находится в видимой области контейнера if(object_right<=container_right && object_left>=container_left) { //--- Если ширина графического объекта не совпадает с шириной элемента, //--- модифицируем графический объект по ширине элемента if(this.ObjectWidth()!=this.Width()) this.ObjectResizeW(this.Width()); } } } } //--- Проверяем выход объекта по горизонтали и вертикали за пределы контейнера bool modified_horizontal=false; // Флаг изменений по горизонтали bool modified_vertical =false; // Флаг изменений по вертикали //--- Обрезка по горизонтали int new_left = object_left; int new_width = this.Width(); //--- Если объект выходит за левую границу контейнера if(object_left<=container_left) { int crop_left=container_left-object_left; new_left=container_left; new_width-=crop_left; modified_horizontal=true; } //--- Если объект выходит за правую границу контейнера if(object_right>=container_right) { int crop_right=object_right-container_right; new_width-=crop_right; modified_horizontal=true; } //--- Если были изменения по горизонтали if(modified_horizontal) { this.ObjectSetX(new_left); this.ObjectResizeW(new_width); } //--- Обрезка по вертикали int new_top=object_top; int new_height=this.Height(); //--- Если объект выходит за верхнюю границу контейнера if(object_top<=container_top) { int crop_top=container_top-object_top; new_top=container_top; new_height-=crop_top; modified_vertical=true; } //--- Если объект выходит за нижнюю границу контейнера if(object_bottom>=container_bottom) { int crop_bottom=object_bottom-container_bottom; new_height-=crop_bottom; modified_vertical=true; } //--- Если были изменения по вертикали if(modified_vertical) { this.ObjectSetY(new_top); this.ObjectResizeH(new_height); } //--- После рассчётов, объект может быть скрыт, но теперь находится в области контейнера - отображаем его this.Show(false); //--- Если объект был изменен, перерисовываем его if(modified_horizontal || modified_vertical) { this.Update(false); this.Draw(false); return true; } return false; }
В первую очередь метод сделали с типом bool, чтобы иметь возможность после работы метода понимать о необходимости перерисовки графика. В различных режимах тестирования обнаружилась недоработка метода, проявляющая себя в том, что обрезаемые элементы не восстанавливали свои размеры. Это происходило в случае, если элемент выходил за пределы контейнера, а потом опять возвращался в видимую область контейнера. Обрезка элемента производится путём изменения координат и размеров его графического объекта. После того, как размеры были изменены, они заново не восстанавливались. Теперь это исправлено.
Метод, устанавливающий z-ордер графического объекта:
//+------------------------------------------------------------------+ //| CCanvasBase::Устанавливает z-ордер графического объекта | //+------------------------------------------------------------------+ bool CCanvasBase::ObjectSetZOrder(const int value) { //--- Если передано уже установленное значение - возвращаем true if(this.ObjectZOrder()==value) return true; //--- Если не удалось установить новое значение в графические объекты фона и переднего плана - возвращаем false if(!::ObjectSetInteger(this.m_chart_id,this.NameBG(),OBJPROP_ZORDER,value) || !::ObjectSetInteger(this.m_chart_id,this.NameFG(),OBJPROP_ZORDER,value)) return false; //--- Записываем новое значение z-ордер в переменную и возвращаем true this.m_z_order=value; return true; }
Сначала переданное значение z-order устанавливается графическим объектам фона и переднего плана, затем — в переменную. Если в графические объекты значение установить не удалось, то уходим из метода с возвратом false.
Методы для изменения размеров графического элемента:
//+------------------------------------------------------------------+ //| CCanvasBase::Изменяет ширину объекта | //+------------------------------------------------------------------+ bool CCanvasBase::ResizeW(const int w) { if(!this.ObjectResizeW(w)) return false; this.BoundResizeW(w); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } return true; } //+------------------------------------------------------------------+ //| CCanvasBase::Изменяет высоту объекта | //+------------------------------------------------------------------+ bool CCanvasBase::ResizeH(const int h) { if(!this.ObjectResizeH(h)) return false; this.BoundResizeH(h); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } return true; } //+------------------------------------------------------------------+ //| CCanvasBase::Изменяет размеры объекта | //+------------------------------------------------------------------+ bool CCanvasBase::Resize(const int w,const int h) { if(!this.ObjectResize(w,h)) return false; this.BoundResize(w,h); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } return true; }
Если физический размер графического объекта изменить не удалось, метод возвращает false. При успешном изменении размера графического объекта, в объект прямоугольной области, описывающий размер элемента, устанавливаем новые значения и вызываем метод обрезки элемента по границам контейнера. Если метод ObjectTrim вернул false — значит либо он ничего не изменил в объекте, либо этот объект не изменяемый. В этом случае объект всё равно нужно обновить и перерисовать, но без перерисовки графика. В итоге возвращаем true.
В методах перемещения элемента, неизменяемая координата должна корректироваться по фактическому её расположению относительно контейнера, что и делают методы AdjX и AdjY:
//+------------------------------------------------------------------+ //| CCanvasBase::Устанавливает объекту новую координату X | //+------------------------------------------------------------------+ bool CCanvasBase::MoveX(const int x) { return this.Move(x,this.AdjY(this.ObjectY())); } //+------------------------------------------------------------------+ //| CCanvasBase::Устанавливает объекту новую координату Y | //+------------------------------------------------------------------+ bool CCanvasBase::MoveY(const int y) { return this.Move(this.AdjX(this.ObjectX()),y); }
В методе инициализации класса инициализируем миллисекундный таймер:
//+------------------------------------------------------------------+ //| CCanvasBase::Инициализация класса | //+------------------------------------------------------------------+ void CCanvasBase::Init(void) { //--- Запоминаем разрешения для мышки и инструментов графика this.m_chart_mouse_wheel_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL); this.m_chart_mouse_move_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE); this.m_chart_object_create_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE); this.m_chart_mouse_scroll_flag = ::ChartGetInteger(this.m_chart_id, CHART_MOUSE_SCROLL); this.m_chart_context_menu_flag = ::ChartGetInteger(this.m_chart_id, CHART_CONTEXT_MENU); this.m_chart_crosshair_tool_flag= ::ChartGetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL); //--- Устанавливаем разрешения для мышки и графика ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, true); ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, true); ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, true); //--- Инициализируем цвета объекта по умолчанию this.InitColors(); //--- Инициализируем миллисекундный таймер ::EventSetMillisecondTimer(16); }
Теперь доработаем обработчик событий класса. Сделаем привычное поведение для пользователя как в операционной системе. При наведении курсора на активный элемент, его цвет должен поменяться, при уводе курсора — вернуться к начальному цвету. При нажатии кнопкой мышки на элементе, его цвет так же меняется и элемент готов к взаимодействию. Если это простая кнопка, то отпускание кнопки мышки в области кнопки приведёт к генерации события щелчка. Если же при нажатой и удерживаемой кнопке увести курсор с объекта, то его цвет изменится, а при отпускании кнопки событие щелчка сгенерировано не будет. Если же это перемещаемый элемент, то удержание кнопки в нажатом положении и перемещение курсора вызовет перемещение удерживаемого элемента. И неважно, находится ли курсор в момент удержания на объекте, либо за его пределами — элемент будет перемещаться пока не будет отпущена кнопка мышки.
Посмотрим какие для этого были внесены доработки в обработчик событий класса:
//+------------------------------------------------------------------+ //| CCanvasBase::Обработчик событий | //+------------------------------------------------------------------+ void CCanvasBase::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- Событие изменения графика if(id==CHARTEVENT_CHART_CHANGE) { //--- скорректируем дистанцию между верхней рамкой подокна индикатора и верхней рамкой главного окна графика this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd); } //--- Событие создания графического объекта if(id==CHARTEVENT_OBJECT_CREATE) { this.OnCreateEvent(id,lparam,dparam,sparam); } //--- Если элемент заблокирован или скрыт - уходим if(this.IsBlocked() || this.IsHidden()) return; //--- Координаты курсора мышки int x=(int)lparam; int y=(int)dparam-this.m_wnd_y; // Корректируем Y по высоте окна индикатора //--- Событие перемещения курсора if(id==CHARTEVENT_MOUSE_MOVE) { //--- Неактивные элементы, кроме главного, не обрабатываем if(!this.IsMain() && (this.Type()<ACTIVE_ELEMENT_MIN || this.Type()>ACTIVE_ELEMENT_MAX)) return; //--- Кнопка мышки удерживается if(sparam=="1") { //--- Курсор в пределах объекта if(this.Contains(x, y)) { //--- Если это главный объект - запрещаем инструменты графика if(this.IsMain()) this.SetFlags(false); //--- Если кнопка мышки была зажата на графике - обрабатывать нечего, уходим if(this.ActiveElementName()=="Chart") return; //--- Фиксируем имя активного элемента, над которым был курсор при нажатии кнопки мышки this.SetActiveElementName(this.ActiveElementName()); //--- Если это текущий активный элемент - обрабатываем его перемещение if(this.IsCurrentActiveElement()) { this.OnMoveEvent(id,lparam,dparam,sparam); //--- Если у элемента активен автоповтор событий - указываем, что кнопка нажата if(this.m_autorepeat_flag) this.m_autorepeat.OnButtonPress(); } } //--- Курсор за пределами объекта else { //--- Если это активный главный объект, либо кнопка мышки зажата на графике - разрешаем инструменты графика if(this.IsMain() && (this.ActiveElementName()==this.NameFG() || this.ActiveElementName()=="Chart")) this.SetFlags(true); //--- Если это текущий активный элемент if(this.IsCurrentActiveElement()) { //--- Если элемент неперемещаемый if(!this.IsMovable()) { //--- вызываем обработчик наведения курсора мышки this.OnFocusEvent(id,lparam,dparam,sparam); //--- Если у элемента активен автоповтор событий - указываем, что кнопка отжата if(this.m_autorepeat_flag) this.m_autorepeat.OnButtonRelease(); } //--- Если элемент перемещаемый - вызываем обработчик перемещения else this.OnMoveEvent(id,lparam,dparam,sparam); } } } //--- Кнопка мышки не нажата else { //--- Курсор в пределах объекта if(this.Contains(x, y)) { //--- Если это главный элемент - отключаем инструменты графика if(this.IsMain()) this.SetFlags(false); //--- Вызываем обработчик наведения курсора и //--- устанавливаем элемент как текущий активный this.OnFocusEvent(id,lparam,dparam,sparam); this.SetActiveElementName(this.NameFG()); } //--- Курсор за пределами объекта else { //--- Если это главный объект if(this.IsMain()) { //--- Разрешаем инструменты графика и //--- устанавливаем график как текущий активный элемент this.SetFlags(true); this.SetActiveElementName("Chart"); } //--- Вызываем обработчик увода курсора из фокуса this.OnReleaseEvent(id,lparam,dparam,sparam); } } } //--- Событие щелчка кнопкой мышки на объекте (отпускание кнопки) if(id==CHARTEVENT_OBJECT_CLICK) { //--- Если щелчок (отпускание кнопки мышки) был по этому объекту if(sparam==this.NameFG()) { //--- Вызываем обработчик щелчка мышки и освобождаем текущий активный объект this.OnPressEvent(id, lparam, dparam, sparam); this.SetActiveElementName(""); //--- Если у элемента активен автоповтор событий - указываем, что кнопка отжата if(this.m_autorepeat_flag) this.m_autorepeat.OnButtonRelease(); } } //--- Событие прокрутки колёсика мышки if(id==CHARTEVENT_MOUSE_WHEEL) { if(this.IsCurrentActiveElement()) this.OnWheelEvent(id,lparam,dparam,sparam); } //--- Если пришло пользовательское событие графика if(id>CHARTEVENT_CUSTOM) { //--- собственные события не обрабатываем if(sparam==this.NameFG()) return; //--- приводим пользовательское событие в соответствие со стандартными ENUM_CHART_EVENT chart_event=ENUM_CHART_EVENT(id-CHARTEVENT_CUSTOM); //--- Если щелчок мышки по объекту - вызываем обработчик пользовательского события if(chart_event==CHARTEVENT_OBJECT_CLICK) { this.MousePressHandler(chart_event, lparam, dparam, sparam); } //--- Если перемещение курсора мышки - вызываем обработчик пользовательского события if(chart_event==CHARTEVENT_MOUSE_MOVE) { this.MouseMoveHandler(chart_event, lparam, dparam, sparam); } //--- Если прокрутка колёсика мышки - вызываем обработчик пользовательского события if(chart_event==CHARTEVENT_MOUSE_WHEEL) { this.MouseWheelHandler(chart_event, lparam, dparam, sparam); } //--- Если изменение графического элемента - вызываем обработчик пользовательского события if(chart_event==CHARTEVENT_OBJECT_CHANGE) { this.ObjectChangeHandler(chart_event, lparam, dparam, sparam); } } }
Вся логика метода расписана в комментариях к коду и на данный момент соответствует заявленному функционалу.
Обработчик перемещения курсора:
//+------------------------------------------------------------------+ //| CCanvasBase::Обработчик перемещения курсора | //+------------------------------------------------------------------+ void CCanvasBase::OnMoveEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Элемент в фокусе при щелчке по нему this.m_focused=true; //--- Если цвета объекта не для режима Pressed if(!this.CheckColor(COLOR_STATE_PRESSED)) { //--- устанавливаем цвета Pressed и перерисовываем объект this.ColorChange(COLOR_STATE_PRESSED); this.Draw(true); } //--- Рассчитываем отступ курсора от верхнего левого угла элемента по осям X и Y if(this.m_cursor_delta_x==0) this.m_cursor_delta_x=(int)lparam-this.X(); if(this.m_cursor_delta_y==0) this.m_cursor_delta_y=(int)::round(dparam-this.Y()); }
При удержании кнопки мышки на элементе, устанавливаем флаг, что элемент в состоянии нажатия. Изменяем цвет элемента и рассчитываем дистанцию курсора от левого верхнего угла элемента по двум осям. Эти смещения будут использоваться при обработке перемещения курсора мышки — чтобы объект смещался за курсором, привязываясь к нему не точкой начала координат (верхний левый угол), а с отступом, записанным в этих переменных.
В обработчиках наведения курсора, ухода из фокуса и нажатия на объект значения отступов инициализируем нулями:
//+------------------------------------------------------------------+ //| CCanvasBase::Обработчик ухода из фокуса | //+------------------------------------------------------------------+ void CCanvasBase::OnReleaseEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Элемент не в фокусе при уводе курсора this.m_focused=false; //--- восстанавливаем исходные цвета, сбрасываем флаг Focused и перерисовываем объект if(!this.CheckColor(COLOR_STATE_DEFAULT)) { this.ColorChange(COLOR_STATE_DEFAULT); this.Draw(true); } //--- Инициализируем отступ курсора от верхнего левого угла элемента по осям X и Y this.m_cursor_delta_x=0; this.m_cursor_delta_y=0; } //+------------------------------------------------------------------+ //| CCanvasBase::Обработчик наведения курсора | //+------------------------------------------------------------------+ void CCanvasBase::OnFocusEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Элемент в фокусе this.m_focused=true; //--- Если цвета объекта не для режима Focused if(!this.CheckColor(COLOR_STATE_FOCUSED)) { //--- устанавливаем цвета и флаг Focused и перерисовываем объект this.ColorChange(COLOR_STATE_FOCUSED); this.Draw(true); } //--- Инициализируем отступ курсора от верхнего левого угла элемента по осям X и Y this.m_cursor_delta_x=0; this.m_cursor_delta_y=0; } //+------------------------------------------------------------------+ //| CCanvasBase::Обработчик нажатия на объект | //+------------------------------------------------------------------+ void CCanvasBase::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Элемент в фокусе при щелчке по нему this.m_focused=true; //--- Если цвета объекта не для режима Pressed if(!this.CheckColor(COLOR_STATE_PRESSED)) { //--- устанавливаем цвета Pressed и перерисовываем объект this.ColorChange(COLOR_STATE_PRESSED); this.Draw(true); } //--- Инициализируем отступ курсора от верхнего левого угла элемента по осям X и Y this.m_cursor_delta_x=0; this.m_cursor_delta_y=0; //--- отправляем пользовательское событие на график с переданными значениями в lparam, dparam, и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, lparam, dparam, this.NameFG()); }
Кроме инициализации переменных, в обработчике нажатия отправляем на график пользовательское событие щелчка по элементу с именем графического объекта переднего плана.
Такие изменения (и некоторые другие незначительные, но обязательные) коснулись файла с классами базовых объектов.
Теперь доработаем файл классов графических элементов Controls.mqh.
Добавим новые макроподстановки и константы перечисления свойств графических элементов:
//+------------------------------------------------------------------+ //| Макроподстановки | //+------------------------------------------------------------------+ #define DEF_LABEL_W 50 // Ширина текстовой метки по умолчанию #define DEF_LABEL_H 16 // Высота текстовой метки по умолчанию #define DEF_BUTTON_W 60 // Ширина кнопки по умолчанию #define DEF_BUTTON_H 16 // Высота кнопки по умолчанию #define DEF_PANEL_W 80 // Ширина панели по умолчанию #define DEF_PANEL_H 80 // Высота панели по умолчанию #define DEF_SCROLLBAR_TH 13 // Толщина полосы прокрутки по умолчанию #define DEF_THUMB_MIN_SIZE 8 // Минимальная толщина ползунка полосы прокрутки #define DEF_AUTOREPEAT_DELAY 500 // Задержка перед запуском автоповтора #define DEF_AUTOREPEAT_INTERVAL 100 // Частота автоповторов //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_SORT_BY // Сравниваемые свойства { ELEMENT_SORT_BY_ID = BASE_SORT_BY_ID, // Сравнение по идентификатору элемента ELEMENT_SORT_BY_NAME = BASE_SORT_BY_NAME, // Сравнение по наименованию элемента ELEMENT_SORT_BY_X = BASE_SORT_BY_X, // Сравнение по координате X элемента ELEMENT_SORT_BY_Y = BASE_SORT_BY_Y, // Сравнение по координате Y элемента ELEMENT_SORT_BY_WIDTH= BASE_SORT_BY_WIDTH, // Сравнение по ширине элемента ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Сравнение по высоте элемента ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Сравнение по Z-order элемента ELEMENT_SORT_BY_TEXT, // Сравнение по тексту элемента ELEMENT_SORT_BY_COLOR_BG, // Сравнение по цвету фона элемента ELEMENT_SORT_BY_ALPHA_BG, // Сравнение по прозрачности фона элемента ELEMENT_SORT_BY_COLOR_FG, // Сравнение по цвету переднего плана элемента ELEMENT_SORT_BY_ALPHA_FG, // Сравнение по прозрачности переднего плана элемента ELEMENT_SORT_BY_STATE, // Сравнение по состоянию элемента ELEMENT_SORT_BY_GROUP, // Сравнение по группе элемента };
Первые семь констант соответствуют одноимённым константам перечисления свойств базового объекта. Таким образом данное перечисление продолжает список свойств базового объекта.
В классе рисования изображений CImagePainter объявим новый метод для рисования рамок группы элементов:
//--- Очищает область bool Clear(const int x,const int y,const int w,const int h,const bool update=true); //--- Рисует закрашенную стрелку (1) вверх, (2) вниз, (3) влево, (4) вправо bool ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует (1) отмеченный, (2) неотмеченный CheckBox bool CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует (1) отмеченный, (2) неотмеченный RadioButton bool CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Рисует рамку группы элементов bool FrameGroupElements(const int x,const int y,const int w,const int h,const string text, const color clr_text,const color clr_dark,const color clr_light, const uchar alpha,const bool update=true); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
и за пределами тела класса напишем его реализацию:
//+------------------------------------------------------------------+ //| Рисует рамку группы элементов | //+------------------------------------------------------------------+ bool CImagePainter::FrameGroupElements(const int x,const int y,const int w,const int h,const string text, const color clr_text,const color clr_dark,const color clr_light, const uchar alpha,const bool update=true) { //--- Если область изображения не валидна - возвращаем false if(!this.CheckBound()) return false; //--- Корректировка координаты Y int tw=0, th=0; if(text!="" && text!=NULL) this.m_canvas.TextSize(text,tw,th); int shift_v=int(th!=0 ? ::ceil(th/2) : 0); //--- Координаты и размеры рамки int x1=x; // Левый верхний угол области рамки, X int y1=y+shift_v; // Левый верхний угол области рамки, Y int x2=x+w-1; // Правый нижний угол области рамки, X int y2=y+h-1; // Правый нижний угол области рамки, Y //--- Рисуем левую-верхнюю часть рамки int arrx[3], arry[3]; arrx[0]=arrx[1]=x1; arrx[2]=x2-1; arry[0]=y2; arry[1]=arry[2]=y1; this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr_dark, alpha)); arrx[0]++; arrx[1]++; arry[1]++; arry[2]++; this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr_light, alpha)); //--- Рисуем правую-нижнюю часть рамки arrx[0]=arrx[1]=x2-1; arrx[2]=x1+1; arry[0]=y1; arry[1]=arry[2]=y2-1; this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr_dark, alpha)); arrx[0]++; arrx[1]++; arry[1]++; arry[2]++; this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr_light, alpha)); if(tw>0) this.m_canvas.FillRectangle(x+5,y,x+7+tw,y+th,clrNULL); this.m_canvas.TextOut(x+6,y-1,text,::ColorToARGB(clr_text, alpha)); if(update) this.m_canvas.Update(false); return true; }
Метод рисует рельефную рамку двумя цветами, передаваемыми в метод — светлым и тёмным. Если передан пустой текст, то просто рисуется рамка по периметру объекта группы. Если текст что-либо содержит, то верхняя часть рамки рисуется ниже верхней границы группы на половину высоты текста.
Доработаем метод сравнения класса рисования изображений:
//+------------------------------------------------------------------+ //| CImagePainter::Сравнение двух объектов | //+------------------------------------------------------------------+ int CImagePainter::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; const CImagePainter *obj=node; switch(mode) { case ELEMENT_SORT_BY_NAME : return(this.Name() >obj.Name() ? 1 : this.Name() <obj.Name() ? -1 : 0); case ELEMENT_SORT_BY_ALPHA_FG : case ELEMENT_SORT_BY_ALPHA_BG : return(this.Alpha() >obj.Alpha() ? 1 : this.Alpha() <obj.Alpha() ? -1 : 0); case ELEMENT_SORT_BY_X : return(this.X() >obj.X() ? 1 : this.X() <obj.X() ? -1 : 0); case ELEMENT_SORT_BY_Y : return(this.Y() >obj.Y() ? 1 : this.Y() <obj.Y() ? -1 : 0); case ELEMENT_SORT_BY_WIDTH : return(this.Width() >obj.Width() ? 1 : this.Width() <obj.Width() ? -1 : 0); case ELEMENT_SORT_BY_HEIGHT : return(this.Height() >obj.Height() ? 1 : this.Height() <obj.Height() ? -1 : 0); default : return(this.ID() >obj.ID() ? 1 : this.ID() <obj.ID() ? -1 : 0); } }
Теперь сортировка будет по всем имеющимся у объекта свойствам.
Класс списка объектов
В статье этой серии о таблицах "Реализация модели таблицы в MQL5: Применение концепции MVC" мы уже обсуждали класс связанного списка. Просто перенесём этот класс в файл Controls.mqh:
//+------------------------------------------------------------------+ //| Класс связанного списка объектов | //+------------------------------------------------------------------+ class CListObj : public CList { protected: ENUM_ELEMENT_TYPE m_element_type; // Тип создаваемого объекта в CreateElement() public: //--- Установка типа элемента void SetElementType(const ENUM_ELEMENT_TYPE type) { this.m_element_type=type; } //--- Виртуальный метод (1) загрузки списка из файла, (2) создания элемента списка virtual bool Load(const int file_handle); virtual CObject *CreateElement(void); }; //+------------------------------------------------------------------+ //| Загрузка списка из файла | //+------------------------------------------------------------------+ bool CListObj::Load(const int file_handle) { //--- Переменные CObject *node; bool result=true; //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Загрузка и проверка маркера начала списка - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Загрузка и проверка типа списка if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Чтение размера списка (количество объектов) uint num=::FileReadInteger(file_handle,INT_VALUE); //--- Последовательно заново создаём элементы списка с помощью вызова метода Load() объектов node this.Clear(); for(uint i=0; i<num; i++) { //--- Читаем и проверяем маркер начала данных объекта - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return false; //--- Читаем тип объекта this.m_element_type=(ENUM_ELEMENT_TYPE)::FileReadInteger(file_handle,INT_VALUE); node=this.CreateElement(); if(node==NULL) return false; this.Add(node); //--- Сейчас файловый указатель смещён относительно начала маркера объекта на 12 байт (8 - маркер, 4 - тип) //--- Поставим указатель на начало данных объекта и загрузим свойства объекта из файла методом Load() элемента node. if(!::FileSeek(file_handle,-12,SEEK_CUR)) return false; result &=node.Load(file_handle); } //--- Результат return result; } //+------------------------------------------------------------------+ //| Метод создания элемента списка | //+------------------------------------------------------------------+ CObject *CListObj::CreateElement(void) { //--- В зависимости от типа объекта в m_element_type, создаём новый объект switch(this.m_element_type) { case ELEMENT_TYPE_BASE : return new CBaseObj(); // Базовый объект графических элементов case ELEMENT_TYPE_COLOR : return new CColor(); // Объект цвета case ELEMENT_TYPE_COLORS_ELEMENT : return new CColorElement(); // Объект цветов элемента графического объекта case ELEMENT_TYPE_RECTANGLE_AREA : return new CBound(); // Прямоугольная область элемента case ELEMENT_TYPE_IMAGE_PAINTER : return new CImagePainter(); // Объект для рисования изображений case ELEMENT_TYPE_CANVAS_BASE : return new CCanvasBase(); // Базовый объект холста графических элементов case ELEMENT_TYPE_ELEMENT_BASE : return new CElementBase(); // Базовый объект графических элементов case ELEMENT_TYPE_LABEL : return new CLabel(); // Текстовая метка case ELEMENT_TYPE_BUTTON : return new CButton(); // Простая кнопка case ELEMENT_TYPE_BUTTON_TRIGGERED : return new CButtonTriggered(); // Двухпозиционная кнопка case ELEMENT_TYPE_BUTTON_ARROW_UP : return new CButtonArrowUp(); // Кнопка со стрелкой вверх case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return new CButtonArrowDown(); // Кнопка со стрелкой вниз case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return new CButtonArrowLeft(); // Кнопка со стрелкой влево case ELEMENT_TYPE_BUTTON_ARROW_RIGHT: return new CButtonArrowRight(); // Кнопка со стрелкой вправо case ELEMENT_TYPE_CHECKBOX : return new CCheckBox(); // Элемент управления CheckBox case ELEMENT_TYPE_RADIOBUTTON : return new CRadioButton(); // Элемент управления RadioButton case ELEMENT_TYPE_PANEL : return new CPanel(); // Элемент управления Panel case ELEMENT_TYPE_GROUPBOX : return new CGroupBox(); // Элемент управления GroupBox case ELEMENT_TYPE_CONTAINER : return new CContainer(); // Элемент управления GroupBox default : return NULL; } }
В объектах этого класса будем хранить графические элементы, привязываемые к родительскому элементу, и будем реализовывать при необходимости списки различных объектов в составе классов графических элементов. Класс также потребуется в методах загрузки свойств элементов из файлов.
Базовый класс графического элемента
У всех графических элементов есть свойства, присущие каждому из всего списка элементов. Для управления этими свойствами, сохранения их в файл и загрузки из файла вынесем их в отдельный класс, от которого и будут наследоваться все графические элементы. Это упростит дальнейшую их разработку.
Создадим новый базовый класс графического элемента:
//+------------------------------------------------------------------+ //| Базовый класс графического элемента | //+------------------------------------------------------------------+ class CElementBase : public CCanvasBase { protected: CImagePainter m_painter; // Класс рисования int m_group; // Группа элементов public: //--- Возвращает указатель на класс рисования CImagePainter *Painter(void) { return &this.m_painter; } //--- (1) Устанавливает координаты, (2) изменяет размеры области изображения void SetImageXY(const int x,const int y) { this.m_painter.SetXY(x,y); } void SetImageSize(const int w,const int h) { this.m_painter.SetSize(w,h); } //--- Устанавливает координаты и размеры области изображения void SetImageBound(const int x,const int y,const int w,const int h) { this.SetImageXY(x,y); this.SetImageSize(w,h); } //--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) правую, (6) нижнюю границу области изображения int ImageX(void) const { return this.m_painter.X(); } int ImageY(void) const { return this.m_painter.Y(); } int ImageWidth(void) const { return this.m_painter.Width(); } int ImageHeight(void) const { return this.m_painter.Height(); } int ImageRight(void) const { return this.m_painter.Right(); } int ImageBottom(void) const { return this.m_painter.Bottom(); } //--- (1) Устанавливает, (2) возвращает группу элементов virtual void SetGroup(const int group) { this.m_group=group; } int Group(void) const { return this.m_group; } //--- Возвращает описание объекта virtual string Description(void); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_ELEMENT_BASE);} //--- Конструкторы/деструктор CElementBase(void) {} CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CElementBase(void) {} };
Параметрический конструктор:
//+----------------------------------------------------------------------+ //| CElementBase::Конструктор параметрический. Строит элемент в указанном| //| окне указанного графика с указанными текстом, координами и размерами | //+----------------------------------------------------------------------+ CElementBase::CElementBase(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CCanvasBase(object_name,chart_id,wnd,x,y,w,h),m_group(-1) { //--- Объекту рисования назначаем канвас переднего плана и //--- обнуляем координаты и размеры, что делает его неактивным this.m_painter.CanvasAssign(this.GetForeground()); this.m_painter.SetXY(0,0); this.m_painter.SetSize(0,0); }
В списке инициализации в конструктор родительского класса передаются значения формальных параметров конструктора, и далее назначается канвас для рисования изображений, размер которого обнуляется. При необходимости использования объекта рисования, нужно установить ему координаты и размеры. Нулевой размер области рисования делает её неактивной.
Метод сравнения двух объектов:
//+------------------------------------------------------------------+ //| CElementBase::Сравнение двух объектов | //+------------------------------------------------------------------+ int CElementBase::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; const CElementBase *obj=node; switch(mode) { case ELEMENT_SORT_BY_NAME : return(this.Name() >obj.Name() ? 1 : this.Name() <obj.Name() ? -1 : 0); case ELEMENT_SORT_BY_X : return(this.X() >obj.X() ? 1 : this.X() <obj.X() ? -1 : 0); case ELEMENT_SORT_BY_Y : return(this.Y() >obj.Y() ? 1 : this.Y() <obj.Y() ? -1 : 0); case ELEMENT_SORT_BY_WIDTH : return(this.Width() >obj.Width() ? 1 : this.Width() <obj.Width() ? -1 : 0); case ELEMENT_SORT_BY_HEIGHT : return(this.Height() >obj.Height() ? 1 : this.Height() <obj.Height() ? -1 : 0); case ELEMENT_SORT_BY_COLOR_BG : return(this.BackColor() >obj.BackColor() ? 1 : this.BackColor() <obj.BackColor() ? -1 : 0); case ELEMENT_SORT_BY_COLOR_FG : return(this.ForeColor() >obj.ForeColor() ? 1 : this.ForeColor() <obj.ForeColor() ? -1 : 0); case ELEMENT_SORT_BY_ALPHA_BG : return(this.AlphaBG() >obj.AlphaBG() ? 1 : this.AlphaBG() <obj.AlphaBG() ? -1 : 0); case ELEMENT_SORT_BY_ALPHA_FG : return(this.AlphaFG() >obj.AlphaFG() ? 1 : this.AlphaFG() <obj.AlphaFG() ? -1 : 0); case ELEMENT_SORT_BY_STATE : return(this.State() >obj.State() ? 1 : this.State() <obj.State() ? -1 : 0); case ELEMENT_SORT_BY_GROUP : return(this.Group() >obj.Group() ? 1 : this.Group() <obj.Group() ? -1 : 0); case ELEMENT_SORT_BY_ZORDER : return(this.ObjectZOrder() >obj.ObjectZOrder() ? 1 : this.ObjectZOrder() <obj.ObjectZOrder() ? -1 : 0); default : return(this.ID() >obj.ID() ? 1 : this.ID() <obj.ID() ? -1 : 0); } }
Метод сравнивает два объекта по всем доступным свойствам.
Метод, возвращающий описание объекта:
//+------------------------------------------------------------------+ //| CElementBase::Возвращает описание объекта | //+------------------------------------------------------------------+ string CElementBase::Description(void) { string nm=this.Name(); string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm); string area=::StringFormat("x %d, y %d, w %d, h %d",this.X(),this.Y(),this.Width(),this.Height()); return ::StringFormat("%s%s (%s, %s): ID %d, Group %d, %s",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.NameBG(),this.NameFG(),this.ID(),this.Group(),area); }
Метод создаёт и возвращает строку из некоторых свойств объекта, удобных для отладки, например:
Container "Main" (ContainerBG, ContainerFG): ID 1, Group -1, x 100, y 40, w 300, h 200
Объект контейнер с пользовательским именем "Main", с именами канваса фона ContainerBG и переднего плана ContainerFG; идентификатор объекта 1, группа -1 (не назначена), координата x 100, y 40, ширина 300, высота 200.
Методы для работы с файлами:
//+------------------------------------------------------------------+ //| CElementBase::Сохранение в файл | //+------------------------------------------------------------------+ bool CElementBase::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CCanvasBase::Save(file_handle)) return false; //--- Сохраняем объект изображения if(!this.m_painter.Save(file_handle)) return false; //--- Сохраняем группу if(::FileWriteInteger(file_handle,this.m_group,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CElementBase::Загрузка из файла | //+------------------------------------------------------------------+ bool CElementBase::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CCanvasBase::Load(file_handle)) return false; //--- Загружаем объект изображения if(!this.m_painter.Load(file_handle)) return false; //--- Загружаем группу this.m_group=::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
Доработка простых элементов управления
Так как некоторые свойства теперь перенесены в новый базовый объект графических элементов, то удалим их из класса объекта текстовой метки:
//+------------------------------------------------------------------+ //| Класс текстовой метки | //+------------------------------------------------------------------+ class CLabel : public CCanvasBase { protected: CImagePainter m_painter; // Класс рисования ushort m_text[]; // Текст ushort m_text_prev[]; // Прошлый текст int m_text_x; // Координата X текста (смещение относительно левой границы объекта) int m_text_y; // Координата Y текста (смещение относительно верхней границы объекта) //--- (1) Устанавливает, (2) возвращает прошлый текст void SetTextPrev(const string text) { ::StringToShortArray(text,this.m_text_prev); } string TextPrev(void) const { return ::ShortArrayToString(this.m_text_prev);} //--- Стирает текст void ClearText(void); public: //--- Возвращает указатель на класс рисования CImagePainter *Painter(void) { return &this.m_painter; } //--- (1) Устанавливает, (2) возвращает текст void SetText(const string text) { ::StringToShortArray(text,this.m_text); } string Text(void) const { return ::ShortArrayToString(this.m_text); } //--- Возвращает координату (1) X, (2) Y текста int TextX(void) const { return this.m_text_x; } int TextY(void) const { return this.m_text_y; } //--- Устанавливает координату (1) X, (2) Y текста void SetTextShiftH(const int x) { this.m_text_x=x; } void SetTextShiftV(const int y) { this.m_text_y=y; } //--- (1) Устанавливает координаты, (2) изменяет размеры области изображения void SetImageXY(const int x,const int y) { this.m_painter.SetXY(x,y); } void SetImageSize(const int w,const int h) { this.m_painter.SetSize(w,h); } //--- Устанавливает координаты и размеры области изображения void SetImageBound(const int x,const int y,const int w,const int h) { this.SetImageXY(x,y); this.SetImageSize(w,h); } //--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) правую, (6) нижнюю границу области изображения int ImageX(void) const { return this.m_painter.X(); } int ImageY(void) const { return this.m_painter.Y(); } int ImageWidth(void) const { return this.m_painter.Width(); } int ImageHeight(void) const { return this.m_painter.Height(); } int ImageRight(void) const { return this.m_painter.Right(); } int ImageBottom(void) const { return this.m_painter.Bottom(); } //--- Выводит текст void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_LABEL); } //--- Конструкторы/деструктор CLabel(void); CLabel(const string object_name, const string text, const int x, const int y, const int w, const int h); CLabel(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CLabel(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h); ~CLabel(void) {} };
А чтобы не прописывать в каждом конструкторе и в каждом объекте установку параметров, вынесем их в отдельные методы:
//+------------------------------------------------------------------+ //| Класс текстовой метки | //+------------------------------------------------------------------+ class CLabel : public CElementBase { protected: ushort m_text[]; // Текст ushort m_text_prev[]; // Прошлый текст int m_text_x; // Координата X текста (смещение относительно левой границы объекта) int m_text_y; // Координата Y текста (смещение относительно верхней границы объекта) //--- (1) Устанавливает, (2) возвращает прошлый текст void SetTextPrev(const string text) { ::StringToShortArray(text,this.m_text_prev); } string TextPrev(void) const { return ::ShortArrayToString(this.m_text_prev);} //--- Стирает текст void ClearText(void); public: //--- (1) Устанавливает, (2) возвращает текст void SetText(const string text) { ::StringToShortArray(text,this.m_text); } string Text(void) const { return ::ShortArrayToString(this.m_text); } //--- Возвращает координату (1) X, (2) Y текста int TextX(void) const { return this.m_text_x; } int TextY(void) const { return this.m_text_y; } //--- Устанавливает координату (1) X, (2) Y текста void SetTextShiftH(const int x) { this.ClearText(); this.m_text_x=x; } void SetTextShiftV(const int y) { this.ClearText(); this.m_text_y=y; } //--- Выводит текст void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_LABEL); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void){} //--- Конструкторы/деструктор CLabel(void); CLabel(const string object_name, const string text, const int x, const int y, const int w, const int h); CLabel(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CLabel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CLabel(void) {} };
Теперь вместо такого вида всех конструкторов объекта (так было ранее)
//+------------------------------------------------------------------+ //| CLabel::Конструктор по умолчанию. Строит метку в главном окне | //| текущего графика в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CLabel::CLabel(void) : CCanvasBase("Label",::ChartID(),0,0,0,DEF_LABEL_W,DEF_LABEL_H), m_text_x(0), m_text_y(0) { //--- Объекту рисования назначаем канвас переднего плана и //--- обнуляем координаты и размеры, что делает его неактивным this.m_painter.CanvasAssign(this.GetForeground()); this.m_painter.SetXY(0,0); this.m_painter.SetSize(0,0); //--- Устанавливаем текущий и предыдущий текст this.SetText("Label"); this.SetTextPrev(""); //--- Фон - прозрачный, передний план - нет this.SetAlphaBG(0); this.SetAlphaFG(255); }
Все они будут такими:
//+------------------------------------------------------------------+ //| CLabel::Конструктор по умолчанию. Строит метку в главном окне | //| текущего графика в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CLabel::CLabel(void) : CElementBase("Label","Label",::ChartID(),0,0,0,DEF_LABEL_W,DEF_LABEL_H), m_text_x(0), m_text_y(0) { //--- Инициализация this.Init("Label"); } //+------------------------------------------------------------------+ //| CLabel::Конструктор параметрический. Строит метку в главном окне | //| текущего графика с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CLabel::CLabel(const string object_name, const string text,const int x,const int y,const int w,const int h) : CElementBase(object_name,text,::ChartID(),0,x,y,w,h), m_text_x(0), m_text_y(0) { //--- Инициализация this.Init(text); } //+-------------------------------------------------------------------+ //| CLabel::Конструктор параметрический. Строит метку в указанном окне| //| текущего графика с указанными текстом, координами и размерами | //+-------------------------------------------------------------------+ CLabel::CLabel(const string object_name, const string text,const int wnd,const int x,const int y,const int w,const int h) : CElementBase(object_name,text,::ChartID(),wnd,x,y,w,h), m_text_x(0), m_text_y(0) { //--- Инициализация this.Init(text); } //+-------------------------------------------------------------------+ //| CLabel::Конструктор параметрический. Строит метку в указанном окне| //| указанного графика с указанными текстом, координами и размерами | //+-------------------------------------------------------------------+ CLabel::CLabel(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CElementBase(object_name,text,chart_id,wnd,x,y,w,h), m_text_x(0), m_text_y(0) { //--- Инициализация this.Init(text); }
Напишем реализацию метода инициализации:
//+------------------------------------------------------------------+ //| CLabel::Инициализация | //+------------------------------------------------------------------+ void CLabel::Init(const string text) { //--- Устанавливаем текущий и предыдущий текст this.SetText(text); this.SetTextPrev(""); //--- Фон - прозрачный, передний план - нет this.SetAlphaBG(0); this.SetAlphaFG(255); }
Теперь для классов, унаследованных от класса CLabel есть возможность определить метод для установки цветов по умолчанию при помощи виртуального метода InitColors().
Количество сравниваемых свойств в методе сравнения теперь максимально — можно сравнивать по всем имеющимся свойствам графического элемента + текст метки:
//+------------------------------------------------------------------+ //| CLabel::Сравнение двух объектов | //+------------------------------------------------------------------+ int CLabel::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; const CLabel *obj=node; switch(mode) { case ELEMENT_SORT_BY_NAME : return(this.Name() >obj.Name() ? 1 : this.Name() <obj.Name() ? -1 : 0); case ELEMENT_SORT_BY_TEXT : return(this.Text() >obj.Text() ? 1 : this.Text() <obj.Text() ? -1 : 0); case ELEMENT_SORT_BY_X : return(this.X() >obj.X() ? 1 : this.X() <obj.X() ? -1 : 0); case ELEMENT_SORT_BY_Y : return(this.Y() >obj.Y() ? 1 : this.Y() <obj.Y() ? -1 : 0); case ELEMENT_SORT_BY_WIDTH : return(this.Width() >obj.Width() ? 1 : this.Width() <obj.Width() ? -1 : 0); case ELEMENT_SORT_BY_HEIGHT : return(this.Height() >obj.Height() ? 1 : this.Height() <obj.Height() ? -1 : 0); case ELEMENT_SORT_BY_COLOR_BG : return(this.BackColor() >obj.BackColor() ? 1 : this.BackColor() <obj.BackColor() ? -1 : 0); case ELEMENT_SORT_BY_COLOR_FG : return(this.ForeColor() >obj.ForeColor() ? 1 : this.ForeColor() <obj.ForeColor() ? -1 : 0); case ELEMENT_SORT_BY_ALPHA_BG : return(this.AlphaBG() >obj.AlphaBG() ? 1 : this.AlphaBG() <obj.AlphaBG() ? -1 : 0); case ELEMENT_SORT_BY_ALPHA_FG : return(this.AlphaFG() >obj.AlphaFG() ? 1 : this.AlphaFG() <obj.AlphaFG() ? -1 : 0); case ELEMENT_SORT_BY_STATE : return(this.State() >obj.State() ? 1 : this.State() <obj.State() ? -1 : 0); case ELEMENT_SORT_BY_ZORDER : return(this.ObjectZOrder() >obj.ObjectZOrder() ? 1 : this.ObjectZOrder() <obj.ObjectZOrder() ? -1 : 0); default : return(this.ID() >obj.ID() ? 1 : this.ID() <obj.ID() ? -1 : 0); } }
В методах для работы с файлами теперь обращаемся к методу родительского класса не CCanvasBase а к новому — CElementBase:
//+------------------------------------------------------------------+ //| CLabel::Сохранение в файл | //+------------------------------------------------------------------+ bool CLabel::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CElementBase::Save(file_handle)) return false; //--- Сохраняем текст if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text)) return false; //--- Сохраняем предыдущий текст if(::FileWriteArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev)) return false; //--- Сохраняем координату X текста if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE) return false; //--- Сохраняем координату Y текста if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CLabel::Загрузка из файла | //+------------------------------------------------------------------+ bool CLabel::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CElementBase::Load(file_handle)) return false; //--- Загружаем текст if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text)) return false; //--- Загружаем предыдущий текст if(::FileReadArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev)) return false; //--- Загружаем координату X текста this.m_text_x=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем координату Y текста this.m_text_y=::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
В классе простой кнопки объявим те же два метода инициализации и обработчик событий таймера:
//+------------------------------------------------------------------+ //| Класс простой кнопки | //+------------------------------------------------------------------+ class CButton : public CLabel { public: //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle) { return CLabel::Save(file_handle); } virtual bool Load(const int file_handle) { return CLabel::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_BUTTON); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void){} //--- Обработчик события таймера virtual void TimerEventHandler(void); //--- Конструкторы/деструктор CButton(void); CButton(const string object_name, const string text, const int x, const int y, const int w, const int h); CButton(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CButton(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CButton (void) {} };
В обработчике событий таймера будут работать счётчики класса автоповтора событий.
В конструкторах класса так же, как и в классе текстовой метки, просто вызываем метод инициализации класса:
//+------------------------------------------------------------------+ //| CButton::Конструктор по умолчанию. Строит кнопку в главном окне | //| текущего графика в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CButton::CButton(void) : CLabel("Button","Button",::ChartID(),0,0,0,DEF_BUTTON_W,DEF_BUTTON_H) { //--- Инициализация this.Init(""); } //+-------------------------------------------------------------------+ //| CButton::Конструктор параметрический. Строит кнопку в главном окне| //| текущего графика с указанными текстом, координами и размерами | //+-------------------------------------------------------------------+ CButton::CButton(const string object_name,const string text,const int x,const int y,const int w,const int h) : CLabel(object_name,text,::ChartID(),0,x,y,w,h) { //--- Инициализация this.Init(""); } //+---------------------------------------------------------------------+ //| CButton::Конструктор параметрический. Строит кнопку в указанном окне| //| текущего графика с указанными текстом, координами и размерами | //+---------------------------------------------------------------------+ CButton::CButton(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) : CLabel(object_name,text,::ChartID(),wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+---------------------------------------------------------------------+ //| CButton::Конструктор параметрический. Строит кнопку в указанном окне| //| указанного графика с указанными текстом, координами и размерами | //+---------------------------------------------------------------------+ CButton::CButton(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CLabel(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(""); }
Так как все кнопки разных типов унаследованы от этого класса простой кнопки, то достаточно взвести флаг использования автоповтора событий и инициализировать объект класса автоповтора событий. И тогда кнопка будет наделена этим функционалом. Здесь, для простой кнопки, флаг использования автоповтора событий сброшен в методе инициализации:
//+------------------------------------------------------------------+ //| CButton::Инициализация | //+------------------------------------------------------------------+ void CButton::Init(const string text) { //--- Устанавливаем состояние по умолчанию this.SetState(ELEMENT_STATE_DEF); //--- Фон и передний план - непрозрачные this.SetAlpha(255); //--- Смещение текста от левого края кнопки по умолчанию this.m_text_x=2; //--- Автоповтор нажатий отключен this.m_autorepeat_flag=false; }
Метод сравнения двух объектов возвращает результат вызова одноимённого метода родительского класса:
//+------------------------------------------------------------------+ //| CButton::Сравнение двух объектов | //+------------------------------------------------------------------+ int CButton::Compare(const CObject *node,const int mode=0) const { return CLabel::Compare(node,mode); }
По сути, можно просто удалить из этого класса объявление и реализацию этого виртуального метода, и будет работать точно так же — будет вызван метод родительского класса. Но пока оставим так, так как библиотека ещё в разработке, и может быть, потребуется здесь доработка этого метода. В итоге по окончании разработки будет видна надобность этого метода здесь (и в последующих классах простых элементов, которые будут дорабатываться).
В обработчике события таймера запускается основной метод класса автоповтора событий в случае, если для класса взведён флаг использования автоповтора событий:
//+------------------------------------------------------------------+ //| Обработчик события таймера | //+------------------------------------------------------------------+ void CButton::TimerEventHandler(void) { if(this.m_autorepeat_flag) this.m_autorepeat.Process(); }
Изменения класса двухпозиционной кнопки:
//+------------------------------------------------------------------+ //| Класс двухпозиционной кнопки | //+------------------------------------------------------------------+ class CButtonTriggered : public CButton { public: //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle) { return CButton::Save(file_handle); } virtual bool Load(const int file_handle) { return CButton::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_BUTTON_TRIGGERED); } //--- Обработчик событий нажатий кнопок мышки (Press) virtual void OnPressEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void); //--- Конструкторы/деструктор CButtonTriggered(void); CButtonTriggered(const string object_name, const string text, const int x, const int y, const int w, const int h); CButtonTriggered(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CButtonTriggered(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CButtonTriggered (void) {} }; //+------------------------------------------------------------------+ //| CButtonTriggered::Конструктор по умолчанию. | //| Строит кнопку в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CButtonTriggered::CButtonTriggered(void) : CButton("Button","Button",::ChartID(),0,0,0,DEF_BUTTON_W,DEF_BUTTON_H) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonTriggered::Конструктор параметрический. | //| Строит кнопку в главном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonTriggered::CButtonTriggered(const string object_name,const string text,const int x,const int y,const int w,const int h) : CButton(object_name,text,::ChartID(),0,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonTriggered::Конструктор параметрический. | //| Строит кнопку в указанном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonTriggered::CButtonTriggered(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,::ChartID(),wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonTriggered::Конструктор параметрический. | //| Строит кнопку в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonTriggered::CButtonTriggered(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonTriggered::Инициализация | //+------------------------------------------------------------------+ void CButtonTriggered::Init(const string text) { //--- Инициализируем цвета по умолчанию this.InitColors(); }
В общем-то здесь просто приведено всё к общему стандарту для классов простых элементов: в конструкторе класса вызывать метод Init(), а уже в нём прописывать необходимые шаги для инициализации класса. Это теперь сделано для всех классов простых графических элементов.
В классах кнопок со стрелками, в их методах инициализации необходимо установить флаги использования автоповтора событий и установить параметры объекта класса автоповтора событий.
Посмотрим как это сделано на примере класса кнопки со стрелкой вверх:
//+------------------------------------------------------------------+ //| Класс кнопки со стрелкой вверх | //+------------------------------------------------------------------+ class CButtonArrowUp : public CButton { public: //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle) { return CButton::Save(file_handle); } virtual bool Load(const int file_handle) { return CButton::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_BUTTON_ARROW_UP);} //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); virtual void InitColors(void){} //--- Конструкторы/деструктор CButtonArrowUp(void); CButtonArrowUp(const string object_name, const string text, const int x, const int y, const int w, const int h); CButtonArrowUp(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CButtonArrowUp(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CButtonArrowUp (void) {} }; //+------------------------------------------------------------------+ //| CButtonArrowUp::Конструктор по умолчанию. | //| Строит кнопку в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CButtonArrowUp::CButtonArrowUp(void) : CButton("Arrow Up Button","",::ChartID(),0,0,0,DEF_BUTTON_W,DEF_BUTTON_H) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonArrowUp::Конструктор параметрический. | //| Строит кнопку в главном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonArrowUp::CButtonArrowUp(const string object_name, const string text,const int x,const int y,const int w,const int h) : CButton(object_name,text,::ChartID(),0,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonArrowUp::Конструктор параметрический. | //| Строит кнопку в указанном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonArrowUp::CButtonArrowUp(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,::ChartID(),wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonArrowUp::Конструктор параметрический. | //| Строит кнопку в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CButtonArrowUp::CButtonArrowUp(const string object_name, const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CButtonArrowUp::Инициализация | //+------------------------------------------------------------------+ void CButtonArrowUp::Init(const string text) { //--- Инициализируем цвета по умолчанию this.InitColors(); //--- Устанавливаем смещение и размеры области изображенеия this.SetImageBound(1,1,this.Height()-2,this.Height()-2); //--- Инициализируем счётчики автоповтора this.m_autorepeat_flag=true; //--- Инициализируем свойства объекта управления автоповтором событий this.m_autorepeat.SetChartID(this.m_chart_id); this.m_autorepeat.SetID(0); this.m_autorepeat.SetName("ButtUpAutorepeatControl"); this.m_autorepeat.SetDelay(DEF_AUTOREPEAT_DELAY); this.m_autorepeat.SetInterval(DEF_AUTOREPEAT_INTERVAL); this.m_autorepeat.SetEvent(CHARTEVENT_OBJECT_CLICK,0,0,this.NameFG()); }
Идентично сделано и в классах кнопок со стрелками вниз, влево и вправо.
Классы-контейнеры для размещения элементов управления
Все ранее созданные элементы являются простыми элементами управления. Они достаточно функциональны, имеют настраиваемое поведение для взаимодействия с пользователем. Но.., это простые элементы управления. Теперь необходимо разработать элементы-контейнеры, которые позволяют прикреплять к себе другие графические компоненты и предоставлять совместное управление связанной группой объектов. И самым первым элементом из списка контейнеров является "Панель".
Класс "Панель"
Графический элемент Панель (Panel) — это базовый контейнерный элемент пользовательского интерфейса, предназначенный для группировки и организации других графических элементов в общей концепции графического интерфейса программы. Панель служит основой для построения сложных элементов: на ней размещаются кнопки, метки, поля ввода и другие элементы управления. С использованием панели можно структурировать визуальное пространство, создавать логические блоки, группы настроек и любые другие составные элементы интерфейса. Панель не только визуально объединяет связанные элементы, но и управляет их положением, видимостью, блокировкой, обработкой событий и сохранением состояния.
Класс панели будет предоставлять возможность размещения на ней множества различных элементов управления. Все элементы, выходящие за границы панели будут обрезаться по её краям. Все манипуляции, выполняемые программно с панелью, будут затрагивать и все элементы управления, входящие в состав панели — скрытие, отображение, перемещение, и т.п.
Продолжим писать код в файле Controls.mqh:
//+------------------------------------------------------------------+ //| Класс панели | //+------------------------------------------------------------------+ class CPanel : public CLabel { private: CElementBase m_temp_elm; // Временный объект для поиска элементов CBound m_temp_bound; // Временный объект для поиска областей protected: CListObj m_list_elm; // Список прикреплённых элементов CListObj m_list_bounds; // Список областей //--- Добавляет новый элемент в список bool AddNewElement(CElementBase *element); public: //--- Возвращает указатель на список (1) прикреплённых элементов, (2) областей CListObj *GetListAttachedElements(void) { return &this.m_list_elm; } CListObj *GetListBounds(void) { return &this.m_list_bounds; } //--- Возвращает элемент по (1) индексу в списке, (2) идентификатору, (3) назначенному имени объекта CElementBase *GetAttachedElementAt(const uint index) { return this.m_list_elm.GetNodeAtIndex(index); } CElementBase *GetAttachedElementByID(const int id); CElementBase *GetAttachedElementByName(const string name); //--- Возвращает область по (1) индексу в списке, (2) идентификатору, (3) назначенному имени области CBound *GetBoundAt(const uint index) { return this.m_list_bounds.GetNodeAtIndex(index); } CBound *GetBoundByID(const int id); CBound *GetBoundByName(const string name); //--- Создаёт и добавляет (1) новый, (2) ранее созданный элемент в список virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h); virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy); //--- Создаёт и добавляет в список новую область CBound *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_PANEL); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Устанавливает объекту новые координаты XY virtual bool Move(const int x,const int y); //--- Смещает объект по осям XY на указанное смещение virtual bool Shift(const int dx,const int dy); //--- (1) Скрывает (2) отображает объект на всех периодах графика, //--- (3) помещает объект на передний план, (4) блокирует, (5) разблокирует элемент, virtual void Hide(const bool chart_redraw); virtual void Show(const bool chart_redraw); virtual void BringToTop(const bool chart_redraw); virtual void Block(const bool chart_redraw); virtual void Unblock(const bool chart_redraw); //--- Выводит в журнал описание объекта virtual void Print(void); //--- Распечатывает список (1) присоединённых объектов, (2) областей void PrintAttached(const uint tab=3); void PrintBounds(void); //--- Обработчик событий virtual void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Обработчик события таймера virtual void TimerEventHandler(void); //--- Конструкторы/деструктор CPanel(void); CPanel(const string object_name, const string text, const int x, const int y, const int w, const int h); CPanel(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); } };
В конструкторах, в строке инициализации все формальные параметры передаются в родительский класс, а затем вызывается метод инициализации объекта:
//+------------------------------------------------------------------+ //| CPanel::Инициализация | //+------------------------------------------------------------------+ void CPanel::Init(void) { //--- Инициализация цветов по умолчанию this.InitColors(); //--- Фон - прозрачный, передний план - нет this.SetAlphaBG(0); this.SetAlphaFG(255); //--- Устанавливаем смещение и размеры области изображенеия this.SetImageBound(0,0,this.Width(),this.Height()); //--- Ширина рамки this.SetBorderWidth(2); }
Метод инициализации цветов объекта по умолчанию:
//+------------------------------------------------------------------+ //| CPanel::Инициализация цветов объекта по умолчанию | //+------------------------------------------------------------------+ void CPanel::InitColors(void) { //--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки this.InitBorderColors(clrNULL,clrNULL,clrNULL,clrNULL); this.InitBorderColorsAct(clrNULL,clrNULL,clrNULL,clrNULL); this.BorderColorToDefault(); //--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента this.InitBorderColorBlocked(clrNULL); this.InitForeColorBlocked(clrSilver); }
В методе сравнения двух объектов возвращается результат работы одноимённого метода родительского класса:
//+------------------------------------------------------------------+ //| CPanel::Сравнение двух объектов | //+------------------------------------------------------------------+ int CPanel::Compare(const CObject *node,const int mode=0) const { return CLabel::Compare(node,mode); }
Метод рисования внешнего вида панели:
//+------------------------------------------------------------------+ //| CPanel::Рисует внешний вид | //+------------------------------------------------------------------+ void CPanel::Draw(const bool chart_redraw) { //--- Заливаем кнопку цветом фона this.Fill(this.BackColor(),false); //--- Очищаем область рисунка this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Задаём цвет для тёмной и светлой линий и рисуем рамку панели color clr_dark =(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),-20,-20,-20)); color clr_light=(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(), 6, 6, 6)); this.m_painter.FrameGroupElements(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()), this.m_painter.Width(),this.m_painter.Height(),this.Text(), this.ForeColor(),clr_dark,clr_light,this.AlphaFG(),true); //--- Обновляем канвас фона без перерисовки графика this.m_background.Update(false); //--- Рисуем элементы списка for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Draw(false); } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Сначала рисуется панель, а затем в цикле все прикреплённые к ней элементы управления.
Метод, добавляющий новый элемент в список:
//+------------------------------------------------------------------+ //| CPanel::Добавляет новый элемент в список | //+------------------------------------------------------------------+ bool CPanel::AddNewElement(CElementBase *element) { //--- Если передан пустой указатель - сообщаем об этом и возвращаем false if(element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return false; } //--- Устанавливаем списку флаг сортировки по идентификатору this.m_list_elm.Sort(ELEMENT_SORT_BY_ID); //--- Если такого элемента нет в списке - возвращаем результат его добавления в список if(this.m_list_elm.Search(element)==NULL) return(this.m_list_elm.Add(element)>-1); //--- Элемент с таким идентификатором уже есть в списке - возвращаем false return false; }
В метод передаётся указатель на элемент, который необходимо поместить в список. Если элемента с установленным ему идентификатором ещё нет в списке — возвращаем результат добавления элемента в список. Иначе — возвращаем false.
Метод, создающий и добавляющий новый элемент в список:
//+------------------------------------------------------------------+ //| CPanel::Создаёт и добавляет новый элемент в список | //+------------------------------------------------------------------+ CElementBase *CPanel::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h) { //--- Создаём имя графического объекта int elm_total=this.m_list_elm.Total(); string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total; //--- Рассчитываем координаты int x=this.X()+dx; int y=this.Y()+dy; //--- В зависимости от типа объекта, создаём новый объект CElementBase *element=NULL; switch(type) { case ELEMENT_TYPE_LABEL : element = new CLabel(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Текстовая метка case ELEMENT_TYPE_BUTTON : element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Простая кнопка case ELEMENT_TYPE_BUTTON_TRIGGERED : element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Двухпозиционная кнопка case ELEMENT_TYPE_BUTTON_ARROW_UP : element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Кнопка со стрелкой вверх case ELEMENT_TYPE_BUTTON_ARROW_DOWN : element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Кнопка со стрелкой вниз case ELEMENT_TYPE_BUTTON_ARROW_LEFT : element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Кнопка со стрелкой влево case ELEMENT_TYPE_BUTTON_ARROW_RIGHT: element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Кнопка со стрелкой вправо case ELEMENT_TYPE_CHECKBOX : element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления CheckBox case ELEMENT_TYPE_RADIOBUTTON : element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления RadioButton case ELEMENT_TYPE_PANEL : element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления Panel case ELEMENT_TYPE_GROUPBOX : element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления GroupBox case ELEMENT_TYPE_SCROLLBAR_THUMB_H : element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Полоса прокрутки горизонтального ScrollBar case ELEMENT_TYPE_SCROLLBAR_THUMB_V : element = new CScrollBarThumbV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Полоса прокрутки вертикального ScrollBar case ELEMENT_TYPE_SCROLLBAR_H : element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления горизонтальный ScrollBar case ELEMENT_TYPE_SCROLLBAR_V : element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления вертикальный ScrollBar case ELEMENT_TYPE_CONTAINER : element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Элемент управления Container default : element = NULL; } //--- Если новый элемент не создан - сообщаем об этом и возвращаем NULL if(element==NULL) { ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type)); return NULL; } //--- Устанавливаем идентификатор, имя, контейнер и z-order элемента element.SetID(elm_total); element.SetName(user_name); element.SetContainerObj(&this); element.ObjectSetZOrder(this.ObjectZOrder()+1); //--- Если созданный элемент не добавлен в список - сообщаем об этом, удаляем созданный элемент и возвращаем NULL if(!this.AddNewElement(element)) { ::PrintFormat("%s: Error. Failed to add %s element with ID %d to list",__FUNCTION__,ElementDescription(type),element.ID()); delete element; return NULL; } //--- Получаем родительский элемент, к которому привязаны дочерние CElementBase *elm=this.GetContainer(); //--- Если родительский элемент имеет тип "Контейнер", значит, у него есть полосы прокрутки if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER) { //--- Преобразуем CElementBase в CContainer CContainer *container_obj=elm; //--- Если горизонтальная полоса прокрутки видима, if(container_obj.ScrollBarHorIsVisible()) { //--- получаем указатель на горизонтальный скроллбар и переносим его на передний план CScrollBarH *sbh=container_obj.GetScrollBarH(); if(sbh!=NULL) sbh.BringToTop(false); } //--- Если вертикальная полоса прокрутки видима, if(container_obj.ScrollBarVerIsVisible()) { //--- получаем указатель на вертикальный скроллбар и переносим его на передний план CScrollBarV *sbv=container_obj.GetScrollBarV(); if(sbv!=NULL) sbv.BringToTop(false); } } //--- Возвращаем указатель на созданный и присоединённый элемент return element; }
В комментариях к методу вся его логика подробно расписана. Замечу, что при создании новых классов новых элементов управления, сюда будем записывать новые типы элементов для их создания.
Метод, добавляющий указанный элемент в список:
//+------------------------------------------------------------------+ //| CPanel::Добавляет указанный элемент в список | //+------------------------------------------------------------------+ CElementBase *CPanel::InsertElement(CElementBase *element,const int dx,const int dy) { //--- Если передан пустой или невалидный указатель на элемент - возвращаем NULL if(::CheckPointer(element)==POINTER_INVALID) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return NULL; } //--- Если передан базовый элемент - возвращаем NULL if(element.Type()==ELEMENT_TYPE_BASE) { ::PrintFormat("%s: Error. The base element cannot be used",__FUNCTION__); return NULL; } //--- Запоминаем идентификатор элемента и устанавливаем новый int id=element.ID(); element.SetID(this.m_list_elm.Total()); //--- Добавляем элемент в список; при неудаче - сообщаем об этом, устанавливаем начальный идентификатор и возвращаем NULL if(!this.AddNewElement(element)) { ::PrintFormat("%s: Error. Failed to add element %s to list",__FUNCTION__,ElementDescription((ENUM_ELEMENT_TYPE)element.Type())); element.SetID(id); return NULL; } //--- Устанавливаем новые координаты, контейнер и z-order элемента int x=this.X()+dx; int y=this.Y()+dy; element.Move(x,y); element.SetContainerObj(&this); element.ObjectSetZOrder(this.ObjectZOrder()+1); //--- Возвращаем указатель на присоединённый элемент return element; }
Метод, в отличие от предыдущего, не создаёт новый элемент, а присоединяет к списку уже существующий, указатель на который передаётся в метод. Если элемент был удалён, либо указатель на элемент имеет значение NULL, производится возврат из метода. Если элемент всё же не удаётся поместить в список, то ему возвращается его идентификатор, который у него был установлен до попытки присоединения к списку. Если же элемент в список добавлен, то ему выставляются новые координаты, указанные в формальных параметрах метода, назначается контейнер и значение z-order.
Метод, возвращающий элемент по идентификатору:
//+------------------------------------------------------------------+ //| CPanel::Возвращает элемент по идентификатору | //+------------------------------------------------------------------+ CElementBase *CPanel::GetAttachedElementByID(const int id) { for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL && elm.ID()==id) return elm; } return NULL; }
В цикле по всем привязанным элементам, ищем элемент с указанным идентификатором. Если найден — возвращаем указатель на элемент в списке. Если не найден — возвращаем NULL.
Метод, возвращающий элемент по назначенному имени объекта:
//+------------------------------------------------------------------+ //| CPanel::Возвращает элемент по назначенному имени объекта | //+------------------------------------------------------------------+ CElementBase *CPanel::GetAttachedElementByName(const string name) { for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL && elm.Name()==name) return elm; } return NULL; }
В цикле по всем привязанным элементам, ищем элемент с указанным пользовательским именем. Если найден — возвращаем указатель на элемент в списке. Если не найден — возвращаем NULL. Метод предлагает удобный поиск по имени, назначенному графическому элементу. Удобнее именовать элемент, и обращаться к нему по его уникальному имени, чем помнить обезличенные идентификаторы для обращения к элементу.
Метод, создающий и добавляющий в список новую область:
//+------------------------------------------------------------------+ //| Создаёт и добавляет в список новую область | //+------------------------------------------------------------------+ CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h) { //--- Проверяем есть ли в списке область с указанным именем и, если да - сообщаем об этом и возвращаем NULL this.m_temp_bound.SetName(name); if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL) { ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name); return NULL; } //--- Создаём новый объект-область; при неудаче - сообщаем об этом и возвращаем NULL CBound *bound=new CBound(dx,dy,w,h); if(bound==NULL) { ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__); return NULL; } //--- Если новый объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL if(this.m_list_bounds.Add(bound)==-1) { ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__); delete bound; return NULL; } //--- Устанавливаем имя области и идентификатор, и возвращаем указатель на объект bound.SetName(name); bound.SetID(this.m_list_bounds.Total()); return bound; }
На панели может быть размечено несколько самостоятельных областей, которые можно контролировать по отдельности. Что разместить в каждой отдельной области — тут решение остаётся за программистом, но наличие раздельных областей добавляет гибкости при планировании и построении графических интерфейсов. Все области хранятся в списке, а вышерассмотренный метод создаёт новый объект области и добавляет его в список, назначая ему имя, передаваемое в формальных параметрах метода, и идентификатор, зависящий от общего количества областей в списке.
Метод, выводящий в журнал описание объекта:
//+------------------------------------------------------------------+ //| Выводит в журнал описание объекта | //+------------------------------------------------------------------+ void CPanel::Print(void) { CBaseObj::Print(); this.PrintAttached(); }
Распечатываем в журнале описание объекта и все привязанные элементы.
Метод, распечатывающий список присоединённых объектов:
//+------------------------------------------------------------------+ //| CPanel::Распечатывает список присоединённых объектов | //+------------------------------------------------------------------+ void CPanel::PrintAttached(const uint tab=3) { //--- В цикле по всем привязанным элементам int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { //--- получаем очередной элемент CElementBase *elm=this.GetAttachedElementAt(i); if(elm==NULL) continue; //--- Получаем тип элемента и, если это полоса прокрутки - пропускаем его ENUM_ELEMENT_TYPE type=(ENUM_ELEMENT_TYPE)elm.Type(); if(type==ELEMENT_TYPE_SCROLLBAR_H || type==ELEMENT_TYPE_SCROLLBAR_V) continue; //--- Распечатываем в журнале описание элемента ::PrintFormat("%*s[%d]: %s",tab,"",i,elm.Description()); //--- Если элемент является контейнером - распечатываем в журнал его список привязанных элементов if(type==ELEMENT_TYPE_PANEL || type==ELEMENT_TYPE_GROUPBOX || type==ELEMENT_TYPE_CONTAINER) { CPanel *obj=elm; obj.PrintAttached(tab*2); } } }
Метод выводит в журнал описания всех элементов, расположенных в списке прикреплённых объектов.
Метод, распечатывающий в журнал список областей:
//+------------------------------------------------------------------+ //| CPanel::Распечатывает список областей | //+------------------------------------------------------------------+ void CPanel::PrintBounds(void) { //--- В цикле по списку областей элемента int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- получаем очередную область и распечатываем её описание в журнал CBound *obj=this.GetBoundAt(i); if(obj==NULL) continue; ::PrintFormat(" [%d]: %s",i,obj.Description()); } }
Метод, устанавливающий объекту новые координаты X и Y:
//+------------------------------------------------------------------+ //| CPanel::Устанавливает объекту новые координаты X и Y | //+------------------------------------------------------------------+ bool CPanel::Move(const int x,const int y) { //--- Вычисляем дистанцию, на которую сместится элемент int delta_x=x-this.X(); int delta_y=y-this.Y(); //--- Перемещаем элемент на указанные координаты bool res=this.ObjectMove(x,y); if(!res) return false; this.BoundMove(x,y); this.ObjectTrim(); //--- Перемещаем все привязанные элементы на рассчитанную дистанцию int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { //--- Перемещаем привязанный элемент с учётом смещения родительского элемента CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) res &=elm.Move(elm.X()+delta_x, elm.Y()+delta_y); } //--- Возвращаем результат перемещения всех привязанных элементов return res; }
Сначала рассчитывается дистанция, на которую будет смещён элемент. Затем элемент смещается на указанные координаты, после чего смещаются все привязанные элементы на рассчитанную в самом начале дистанцию.
Метод, смещающий объект по осям X и Y на указанную дистанцию смещения:
//+------------------------------------------------------------------+ //| CPanel::Смещает объект по осям X и Y на указанное смещение | //+------------------------------------------------------------------+ bool CPanel::Shift(const int dx,const int dy) { //--- Смещаем элемент на указанную дистанцию bool res=this.ObjectShift(dx,dy); if(!res) return false; this.BoundShift(dx,dy); this.ObjectTrim(); //--- Смещаем все привязанные элементы int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) res &=elm.Shift(dx,dy); } //--- Возвращаем результат смещения всех привязанных элементов return res; }
Метод, скрывающий объект на всех периодах графика:
//+------------------------------------------------------------------+ //| CPanel::Скрывает объект на всех периодах графика | //+------------------------------------------------------------------+ void CPanel::Hide(const bool chart_redraw) { //--- Если объект уже скрыт - уходим if(this.m_hidden) return; //--- Скрываем панель CCanvasBase::Hide(false); //--- Скрываем прикреплённые объекты for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Hide(false); } //--- Если указано - перерисовываем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, отображающий объект на всех периодах графика:
//+------------------------------------------------------------------+ //| CPanel::Отображает объект на всех периодах графика | //+------------------------------------------------------------------+ void CPanel::Show(const bool chart_redraw) { //--- Если объект уже видимый - уходим if(!this.m_hidden) return; //--- Отображаем панель CCanvasBase::Show(false); //--- Отображаем прикреплённые объекты for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Show(false); } //--- Если указано - перерисовываем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, помещающий объект на передний план:
//+------------------------------------------------------------------+ //| CPanel::Помещает объект на передний план | //+------------------------------------------------------------------+ void CPanel::BringToTop(const bool chart_redraw) { //--- Помещаем панель на передний план CCanvasBase::BringToTop(false); //--- Помещаем на передний план прикреплённые объекты for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.BringToTop(false); } //--- Если указано - перерисовываем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, блокирующий элемент:
//+------------------------------------------------------------------+ //| CPanel::Блокирует элемент | //+------------------------------------------------------------------+ void CPanel::Block(const bool chart_redraw) { //--- Если элемент уже заблокирован - уходим if(this.m_blocked) return; //--- Блокируем панель CCanvasBase::Block(false); //--- Блокируем прикреплённые объекты for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Block(false); } //--- Если указано - перерисовываем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, разблокирующий элемент:
//+------------------------------------------------------------------+ //| CPanel::Разблокирует элемент | //+------------------------------------------------------------------+ void CPanel::Unblock(const bool chart_redraw) { //--- Если элемент уже разблокирован - уходим if(!this.m_blocked) return; //--- Разблокируем панель CCanvasBase::Unblock(false); //--- Разблокируем прикреплённые объекты for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Unblock(false); } //--- Если указано - перерисовываем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Методы для работы с файлами:
//+------------------------------------------------------------------+ //| CPanel::Сохранение в файл | //+------------------------------------------------------------------+ bool CPanel::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CElementBase::Save(file_handle)) return false; //--- Сохраняем список прикреплённых элементов if(!this.m_list_elm.Save(file_handle)) return false; //--- Сохраняем список областей if(!this.m_list_bounds.Save(file_handle)) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CPanel::Загрузка из файла | //+------------------------------------------------------------------+ bool CPanel::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CElementBase::Load(file_handle)) return false; //--- Загружаем список прикреплённых элементов if(!this.m_list_elm.Load(file_handle)) return false; //--- Загружаем список областей if(!this.m_list_bounds.Load(file_handle)) return false; //--- Всё успешно return true; }
Обработчик событий:
//+------------------------------------------------------------------+ //| CPanel::Обработчик событий | //+------------------------------------------------------------------+ void CPanel::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Вызываем обработчик событий родительского класса CCanvasBase::OnChartEvent(id,lparam,dparam,sparam); //--- В цикле по всем привязанным элементам int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { //--- получаем очередной элемент и вызываем его обработчик событий CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.OnChartEvent(id,lparam,dparam,sparam); } }
Сначала вызывается обработчик событий панели, затем в цикле по списку прикреплённых элементов вызываются обработчики прикреплённых элементов.
Обработчик события таймера:
//+------------------------------------------------------------------+ //| CPanel::Обработчик события таймера | //+------------------------------------------------------------------+ void CPanel::TimerEventHandler(void) { //--- В цикле по всем привязанным элементам for(int i=0;i<this.m_list_elm.Total();i++) { //--- получаем очередной элемент и вызываем его обработчик событий таймера CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.TimerEventHandler(); } }
Сначала вызывается обработчик таймера панели, затем в цикле по списку прикреплённых элементов вызываются обработчики прикреплённых элементов. Если для какого-либо элемента виртуальный обработчик реализован, то он обработает событие таймера. На данный момент такие обработчики реализованы для кнопок.
Следующим элементом управления будет Группа Объектов — GroupBox. Может использоваться для создания "групповых рамок" (group box), которые часто встречаются в интерфейсах программ: это визуальные блоки с заголовком, внутри которых располагаются связанные по смыслу элементы управления (например, набор радиокнопок, чекбоксов, кнопок, полей ввода и т.п.). Такой подход помогает структурировать интерфейс, повысить его читаемость и удобство для пользователя. Класс будет унаследован от класса объекта Панели, что позволит получить от родительского класса все возможности панели: добавление/удаление элементов, управление их положением, обработка событий, сохранение/загрузка состояния и т.д.
Класс группы объектов
Продолжим писать код в файле Controls.mqh:
//+------------------------------------------------------------------+ //| Класс группы объектов | //+------------------------------------------------------------------+ class CGroupBox : public CPanel { public: //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_GROUPBOX); } //--- Инициализация объекта класса void Init(void); //--- Устанавливает группу элементов virtual void SetGroup(const int group); //--- Создаёт и добавляет (1) новый, (2) ранее созданный элемент в список virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h); virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy); //--- Конструкторы/деструктор CGroupBox(void); CGroupBox(const string object_name, const string text, const int x, const int y, const int w, const int h); CGroupBox(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CGroupBox(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CGroupBox(void) {} };
Здесь добавлен новый метод для установки группы и будут переопределены методы создания и добавления элементов в список.
В конструкторах класса, в списке инициализации все значения, переданные в формальных параметрах, передаются в конструктор родительского класса, и далее вызывается метод инициализации:
//+------------------------------------------------------------------+ //| CGroupBox::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CGroupBox::CGroupBox(void) : CPanel("GroupBox","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CGroupBox::Конструктор параметрический. | //| Строит элемент в главном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CGroupBox::CGroupBox(const string object_name,const string text,const int x,const int y,const int w,const int h) : CPanel(object_name,text,::ChartID(),0,x,y,w,h) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CGroupBox::Конструктор параметрический. | //| Строит элемент в указанном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CGroupBox::CGroupBox(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,::ChartID(),wnd,x,y,w,h) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CGroupBox::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CGroupBox::CGroupBox(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(); }
Инициализация элемента производится вызовом метода инициализации родительского класса:
//+------------------------------------------------------------------+ //| CGroupBox::Инициализация | //+------------------------------------------------------------------+ void CGroupBox::Init(void) { //--- Инициализация при помощи родительского класса CPanel::Init(); }
Метод, устанавливающий группу элементов:
//+------------------------------------------------------------------+ //| CGroupBox::Устанавливает группу элементов | //+------------------------------------------------------------------+ void CGroupBox::SetGroup(const int group) { //--- Устанавливаем группу этому элементу методом родительского класса CElementBase::SetGroup(group); //--- В цикле по списку привязанных элементов for(int i=0;i<this.m_list_elm.Total();i++) { //--- получаем очередной элемент и назначаем ему группу CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.SetGroup(group); } }
После установки группы элементу, в цикле по списку прикреплённых объектов устанавливается группа каждому подчинённому элементу.
Метод создания и добавления нового элемента в список:
//+------------------------------------------------------------------+ //| CGroupBox::Создаёт и добавляет новый элемент в список | //+------------------------------------------------------------------+ CElementBase *CGroupBox::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h) { //--- Создаём и добавляем в список элементов новый элемент CElementBase *element=CPanel::InsertNewElement(type,text,user_name,dx,dy,w,h); if(element==NULL) return NULL; //--- Устанавливаем созданному элементу группу, равную группе этого объекта element.SetGroup(this.Group()); return element; }
Сначала в список привязанных объектов создаётся и добавляется новый элемент при помощи метода родительского класса, а затем вновь созданному элементу назначается группа этого объекта.
Метод, добавляющий указанный элемент в список:
//+------------------------------------------------------------------+ //| CGroupBox::Добавляет указанный элемент в список | //+------------------------------------------------------------------+ CElementBase *CGroupBox::InsertElement(CElementBase *element,const int dx,const int dy) { //--- Добавляем в список элементов новый элемент if(CPanel::InsertElement(element,dx,dy)==NULL) return NULL; //--- Устанавливаем добавленному элементу группу, равную группе этого объекта element.SetGroup(this.Group()); return element; }
Здесь по аналогии с предыдущим методом, только в список добавляется переданный в метод указатель на ранее созданный элемент.
Все элементы, добавленные в CGroupBox, автоматически получают одинаковый идентификатор группы, наздаченный панели. Это позволяет реализовать логику, когда, например, только один элемент из группы может быть активен (актуально для радиокнопок), или когда требуется массовое управление состоянием группы элементов. CGroupBox отображает рамку с заголовком, отделяя свою область от остального интерфейса. Метод SetGroup позволяет назначить всей группе новый идентификатор, автоматически устанавливая его всем привязанным элементам.
На очереди класс Контейнер, который используется для создания областей интерфейса, где содержимое может выходить за пределы видимой области. В таких случаях пользователю предоставляется возможность прокручивать содержимое с помощью горизонтальной и/или вертикальной полосы прокрутки. Это особенно актуально для реализации прокручиваемых списков, таблиц, больших форм, графиков и других элементов, размеры которых могут превышать размеры контейнера.
Класс контейнера будет унаследован от класса панели. Но для его создания предварительно необходимо создать вспомогательные элементы: классы вертикальной и горизонтальной полос прокрутки.
Скроллбар — это элемент пользовательского интерфейса, предназначенный для прокрутки содержимого, которое не помещается в видимой области окна (контейнера). Скроллбары позволяют пользователю перемещаться по горизонтали и вертикали, управляя отображением больших списков, таблиц, форм и других элементов. В графических интерфейсах скроллбары предоставляют удобную навигацию, делают работу с большим объемом информации интуитивно понятной и привычной для пользователя.
Классы для создания полос прокрутки
Класс полосы прокрутки — это составные элементы, включающие:
- Две кнопки со стрелками (влево/вправо или вверх/вниз) для пошаговой прокрутки.
- Ползунок (thumb), который можно перетаскивать для быстрой прокрутки.
- Трек (track) — область, по которой перемещается ползунок.
Ползунок полосы прокрутки сделаем из графического элемента Кнопка, трек будет сделан из элемента Панель, ну и уже готовые кнопки со стрелками. К панели (треку) будут привязаны кнопки прокрутки и ползунок.
- При нажатии на кнопки со стрелками ползунок будет сдвигаться на фиксированный шаг, а содержимое контейнера — прокручиваться на рассчитанное значение, пропорциональное сдвигу ползунка относительно трека.
- При перетаскивании ползунка мышью или прокрутке колесиком его положение будет меняться, а содержимое контейнера — смещаться пропорционально.
- Скроллбар будет рассчитывать размеры трека и ползунка в зависимости от размеров содержимого и видимой области контейнера.
Классы ползунков полос прокрутки
Продолжим писать код в файле Controls.mqh.
Класс ползунка горизонтальной полосы прокрутки.
//+------------------------------------------------------------------+ //| Класс ползунка горизонтальной полосы прокрутки | //+------------------------------------------------------------------+ class CScrollBarThumbH : public CButton { protected: bool m_chart_redraw; // Флаг обновления графика public: //--- (1) Устанавливает, (2) возвращает флаг обновления графика void SetChartRedrawFlag(const bool flag) { this.m_chart_redraw=flag; } bool ChartRedrawFlag(void) const { return this.m_chart_redraw; } //--- Виртуальные методы (1) сохранения в файл, (2) загрузки из файла, (3) тип объекта virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_SCROLLBAR_THUMB_H); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); //--- Обработчики событий (1) перемещения курсора, (2) прокрутки колёсика virtual void OnMoveEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Конструкторы/деструктор CScrollBarThumbH(void); CScrollBarThumbH(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CScrollBarThumbH (void) {} };
Класс унаследован от простой кнопки. Соответственно, наследует от неё и обработчики событий. У простой кнопки нет обработки события перемещения и прокрутки колёсика мышки. Поэтому здесь будут реализованы эти виртуальные методы. И здесь добавлен флаг самостоятельного обновления графика. Зачем он нужен? Если использовать этот класс отдельно от контейнера (например, для элементов управления с ползунками), то при изменениях положения ползунка, необходимо перерисовывать график для немедленного отображения изменений. Для этого этот флаг ставится в положение true. В составе же контейнера скроллбарами управляет контейнер, и перерисовкой графика — тоже. В этом случае здесь этот флаг должен быть сброшен (это его значение по умолчанию).
В конструкторах класса, в строке инициализации в конструктор родительского класса устанавливаются значения, переданные в формальных параметрах конструктора, и далее вызывается метод инициализации класса:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CScrollBarThumbH::CScrollBarThumbH(void) : CButton("SBThumb","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_SCROLLBAR_TH) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CScrollBarThumbH::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CScrollBarThumbH::CScrollBarThumbH(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(""); }
Метод инициализации класса:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Инициализация | //+------------------------------------------------------------------+ void CScrollBarThumbH::Init(const string text) { //--- Инициализация родительского класса CButton::Init(""); //--- Устанавливаем флаги перемещаемости и обновления графика this.SetMovable(true); this.SetChartRedrawFlag(false); }
Этот элемент, будучи привязанным к другому элементу, может смещаться. Поэтому для него установлен флаг перемещаемости. Флаг перерисовки графика сброшен по умолчанию. При необходимости может быть установлен в любое время.
Обработчик перемещения курсора:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Обработчик перемещения курсора | //+------------------------------------------------------------------+ void CScrollBarThumbH::OnMoveEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Обработчик перемещения курсора базового объекта CCanvasBase::OnMoveEvent(id,lparam,dparam,sparam); //--- Получаем указатель на базовый объект (элемент управления "горизонтальная полоса прокрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем ширину базового объекта и рассчитываем границы пространства для ползунка int base_w=base_obj.Width(); int base_left=base_obj.X()+base_obj.Height(); int base_right=base_obj.Right()-base_obj.Height()+1; //--- Из координат курсора и размеров ползунка рассчитываем ограничения для перемещения int x=(int)lparam-this.m_cursor_delta_x; if(x<base_left) x=base_left; if(x+this.Width()>base_right) x=base_right-this.Width(); //--- Сдвигаем ползунок на рассчитанную координату X if(!this.MoveX(x)) return; //--- Рассчитываем позицию ползунка int thumb_pos=this.X()-base_left; //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_MOVE, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); }
Здесь по размерам базового объекта рассчитываются размеры трека (Track), внутри которого ползунок может перемещаться. Далее ползунок смещается вслед за курсором, но с учётом ограничений трека. После чего рассчитывается смещение ползунка относительно левого края трека и это значение отправляется на график в пользовательском событии, где указано, что это событие перемещения мышки и имя объекта-ползунка. Эти значения необходимы скроллбару, которому принадлежит ползунок, для дальнейшей работы по смещению содержимого контейнера, которому принадлежит скроллбар соответственно.
Обработчик прокрутки колёсика:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Получаем указатель на базовый объект (элемент управления "горизонтальная полоса прогрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем ширину базового объекта и рассчитываем границы пространства для ползунка int base_w=base_obj.Width(); int base_left=base_obj.X()+base_obj.Height(); int base_right=base_obj.Right()-base_obj.Height()+1; //--- Задаём направление смещения в зависимости от направления вращения колёсика мышки int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dx==0) dx=(int)lparam; //--- Если при смещении ползунок выйдет за левый край своей области - устанавливаем его на левый край if(dx<0 && this.X()+dx<=base_left) this.MoveX(base_left); //--- иначе, если при смещении ползунок выйдет за правый край своей области - позиционируем его по правому краю else if(dx>0 && this.Right()+dx>=base_right) this.MoveX(base_right-this.Width()); //--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения else if(this.ShiftX(dx)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- Рассчитываем позицию ползунка int thumb_pos=this.X()-base_left; //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); }
Логика примерно та же, что и у предыдущего метода. Но отправляется событие прокрутки колёсика.
Методы работы с файлами:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Сохранение в файл | //+------------------------------------------------------------------+ bool CScrollBarThumbH::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CButton::Save(file_handle)) return false; //--- Сохраняем флаг обновления графика if(::FileWriteInteger(file_handle,this.m_chart_redraw,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CScrollBarThumbH::Загрузка из файла | //+------------------------------------------------------------------+ bool CScrollBarThumbH::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CButton::Load(file_handle)) return false; //--- Загружаем флаг обновления графика this.m_chart_redraw=::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
Класс ползунка вертикальной полосы прокрутки:
//+------------------------------------------------------------------+ //| Класс ползунка вертикальной полосы прокрутки | //+------------------------------------------------------------------+ class CScrollBarThumbV : public CButton { protected: bool m_chart_redraw; // Флаг обновления графика public: //--- (1) Устанавливает, (2) возвращает флаг обновления графика void SetChartRedrawFlag(const bool flag) { this.m_chart_redraw=flag; } bool ChartRedrawFlag(void) const { return this.m_chart_redraw; } //--- Виртуальные методы (1) сохранения в файл, (2) загрузки из файла, (3) тип объекта virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_SCROLLBAR_THUMB_V); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(const string text); //--- Обработчики событий (1) перемещения курсора, (2) прокрутки колёсика virtual void OnMoveEvent(const int id, const long lparam, const double dparam, const string sparam); virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Конструкторы/деструктор CScrollBarThumbV(void); CScrollBarThumbV(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CScrollBarThumbV (void) {} }; //+------------------------------------------------------------------+ //| CScrollBarThumbV::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CScrollBarThumbV::CScrollBarThumbV(void) : CButton("SBThumb","",::ChartID(),0,0,0,DEF_SCROLLBAR_TH,DEF_PANEL_W) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CScrollBarThumbV::CScrollBarThumbV(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h) { //--- Инициализация this.Init(""); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Инициализация | //+------------------------------------------------------------------+ void CScrollBarThumbV::Init(const string text) { //--- Инициализация родительского класса CButton::Init(""); //--- Устанавливаем флаги перемещаемости и обновления графика this.SetMovable(true); this.SetChartRedrawFlag(false); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Обработчик перемещения курсора | //+------------------------------------------------------------------+ void CScrollBarThumbV::OnMoveEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Обработчик перемещения курсора базового объекта CCanvasBase::OnMoveEvent(id,lparam,dparam,sparam); //--- Получаем указатель на базовый объект (элемент управления "вертикальная полоса прогрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем высоту базового объекта и рассчитываем границы пространства для ползунка int base_h=base_obj.Height(); int base_top=base_obj.Y()+base_obj.Width(); int base_bottom=base_obj.Bottom()-base_obj.Width()+1; //--- Из координат курсора и размеров ползунка рассчитываем ограничения для перемещения int y=(int)dparam-this.m_cursor_delta_y; if(y<base_top) y=base_top; if(y+this.Height()>base_bottom) y=base_bottom-this.Height(); //--- Сдвигаем ползунок на рассчитанную координату Y if(!this.MoveY(y)) return; //--- Рассчитываем позицию ползунка int thumb_pos=this.Y()-base_top; //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_MOVE, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Получаем указатель на базовый объект (элемент управления "вертикальная полоса прогрутки") CCanvasBase *base_obj=this.GetContainer(); //--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим if(!this.IsMovable() || base_obj==NULL) return; //--- Получаем высоту базового объекта и рассчитываем границы пространства для ползунка int base_h=base_obj.Height(); int base_top=base_obj.Y()+base_obj.Width(); int base_bottom=base_obj.Bottom()-base_obj.Width()+1; //--- Задаём направление смещения в зависимости от направления вращения колёсика мышки int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dy==0) dy=(int)lparam; //--- Если при смещении ползунок выйдет за верхний край своей области - устанавливаем его на верхний край if(dy<0 && this.Y()+dy<=base_top) this.MoveY(base_top); //--- иначе, если при смещении ползунок выйдет за нижний край своей области - позиционируем его по нижнему краю else if(dy>0 && this.Bottom()+dy>=base_bottom) this.MoveY(base_bottom-this.Height()); //--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения else if(this.ShiftY(dy)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- Рассчитываем позицию ползунка int thumb_pos=this.Y()-base_top; //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Перерисовываем график if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Сохранение в файл | //+------------------------------------------------------------------+ bool CScrollBarThumbV::Save(const int file_handle) { //--- Сохраняем данные родительского объекта if(!CButton::Save(file_handle)) return false; //--- Сохраняем флаг обновления графика if(::FileWriteInteger(file_handle,this.m_chart_redraw,INT_VALUE)!=INT_VALUE) return false; //--- Всё успешно return true; } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Загрузка из файла | //+------------------------------------------------------------------+ bool CScrollBarThumbV::Load(const int file_handle) { //--- Загружаем данные родительского объекта if(!CButton::Load(file_handle)) return false; //--- Загружаем флаг обновления графика this.m_chart_redraw=::FileReadInteger(file_handle,INT_VALUE); //--- Всё успешно return true; }
Разница с предыдущим классом заключается только в расчёте ограничений для смещения ползунка, так как здесь он смещается по вертикали. Всё остальное идентично классу ползунка горизонтальной полосы прокрутки.
Классы CScrollBarThumbH (горизонтальный ползунок) и CScrollBarThumbV (вертикальный ползунок) реализуют подвижные элементы в пользовательских интерфейсах. Классы наследуются от класса кнопки, поддерживают перемещение мышью вдоль трека скроллбара, или иного ограничивающего элемента, а также реагируют на прокрутку колесика мыши. При изменении положения ползунка, классы отправляют событие с новой позицией, что позволяет синхронизировать отображение содержимого контейнера. Ползунки ограничены в движении границами трека, могут сохранять своё состояние в файл и загружать его из файла, и управляют необходимостью перерисовки графика. Эти классы обеспечивают интуитивное и привычное взаимодействие пользователя с прокручиваемыми областями интерфейса.
В данном контексте ползунки будут работать в составе классов горизонтальной и вертикальной полос прокрутки.
Класс горизонтальной полосы прокрутки
Продолжим писать код в файле Controls.mqh:
//+------------------------------------------------------------------+ //| Класс горизонтальной полосы прокрутки | //+------------------------------------------------------------------+ class CScrollBarH : public CPanel { protected: CButtonArrowLeft *m_butt_left; // Кнопка со стрелкой влево CButtonArrowRight*m_butt_right; // Кнопка со стрелкой вправо CScrollBarThumbH *m_thumb; // Ползунок скроллбара public: //--- Возвращает указатель на (1) левую, (2) правую кнопку, (3) ползунок CButtonArrowLeft *GetButtonLeft(void) { return this.m_butt_left; } CButtonArrowRight*GetButtonRight(void) { return this.m_butt_right; } CScrollBarThumbH *GetThumb(void) { return this.m_thumb; } //--- (1) Устанавливает, (2) возвращает флаг обновления графика void SetChartRedrawFlag(const bool flag) { if(this.m_thumb!=NULL) this.m_thumb.SetChartRedrawFlag(flag); } bool ChartRedrawFlag(void) const { return(this.m_thumb!=NULL ? this.m_thumb.ChartRedrawFlag() : false); } //--- Возвращает (1) длину (2) начало трека, (3) позицию ползунка int TrackLength(void) const; int TrackBegin(void) const; int ThumbPosition(void) const; //--- Изменяет размер ползунка bool SetThumbSize(const uint size) const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeW(size) : false); } //--- Изменяет ширину объекта virtual bool ResizeW(const int size); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_SCROLLBAR_H); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Обработчик прокрутки колёсика (Wheel) virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Конструкторы/деструктор CScrollBarH(void); CScrollBarH(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CScrollBarH(void) {} };
По объявленным методам видно, что класс имеет указатели на кнопки со стрелками влево/вправо и на объект-ползунок. Все эти объекты создаются в методе инициализации класса. В классе реализованы методы, возвращающие указатели на эти объекты. Также реализованы методы для установки флага обновления графика ползунком скроллбара, и для получения состояния этого флага из объекта ползунка.
В конструкторах класса в строке инициализации значения формальных параметров передаются в конструктор родительского класса, и далее вызывается метод инициализации класса:
//+------------------------------------------------------------------+ //| CScrollBarH::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CScrollBarH::CScrollBarH(void) : CPanel("ScrollBarH","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H),m_butt_left(NULL),m_butt_right(NULL),m_thumb(NULL) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CScrollBarH::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CScrollBarH::CScrollBarH(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_butt_left(NULL),m_butt_right(NULL),m_thumb(NULL) { //--- Инициализация this.Init(); }
Метод инициализации класса:
//+------------------------------------------------------------------+ //| CScrollBarH::Инициализация | //+------------------------------------------------------------------+ void CScrollBarH::Init(void) { //--- Инициализация родительского класса CPanel::Init(); //--- Фон - непрозрачный this.SetAlphaBG(255); //--- Ширина рамки и текст this.SetBorderWidth(0); this.SetText(""); //--- Элемент не обрезается по границам контейнера this.m_trim_flag=false; //--- Создаём кнопки прокрутки int w=this.Height(); int h=this.Height(); this.m_butt_left = this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_LEFT, "","ButtL",0,0,w,h); this.m_butt_right= this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_RIGHT,"","ButtR",this.Width()-w,0,w,h); if(this.m_butt_left==NULL || this.m_butt_right==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Настраиваем цвета и вид кнопки со стрелкой влево this.m_butt_left.SetImageBound(1,1,w-2,h-4); this.m_butt_left.InitBackColors(this.m_butt_left.BackColorFocused()); this.m_butt_left.ColorsToDefault(); this.m_butt_left.InitBorderColors(this.BorderColor(),this.m_butt_left.BackColorFocused(),this.m_butt_left.BackColorPressed(),this.m_butt_left.BackColorBlocked()); this.m_butt_left.ColorsToDefault(); //--- Настраиваем цвета и вид кнопки со стрелкой вправо this.m_butt_right.SetImageBound(1,1,w-2,h-4); this.m_butt_right.InitBackColors(this.m_butt_right.BackColorFocused()); this.m_butt_right.ColorsToDefault(); this.m_butt_right.InitBorderColors(this.BorderColor(),this.m_butt_right.BackColorFocused(),this.m_butt_right.BackColorPressed(),this.m_butt_right.BackColorBlocked()); this.m_butt_right.ColorsToDefault(); //--- Создаём ползунок int tsz=this.Width()-w*2; this.m_thumb=this.InsertNewElement(ELEMENT_TYPE_SCROLLBAR_THUMB_H,"","ThumbH",w,1,tsz-w*4,h-2); if(this.m_thumb==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Настраиваем цвета ползунка и устанавливаем ему флаг перемещаемости this.m_thumb.InitBackColors(this.m_thumb.BackColorFocused()); this.m_thumb.ColorsToDefault(); this.m_thumb.InitBorderColors(this.m_thumb.BackColor(),this.m_thumb.BackColorFocused(),this.m_thumb.BackColorPressed(),this.m_thumb.BackColorBlocked()); this.m_thumb.ColorsToDefault(); this.m_thumb.SetMovable(true); //--- запрещаем самостоятельную перерисовку графика this.m_thumb.SetChartRedrawFlag(false); }
Полосы прокрутки располагаются снизу и справа контейнера за пределами его области видимости, где располагается содержимое контейнера. Все объекты, прикреплённые к контейнеру, всегда обрезаются по границам видимой зоны контейнера. Скроллбары находятся за пределами видимой зоны контейнера, а значит — обрезаются и становятся невидимы. Чтобы избежать такого поведения, у всех объектов существует флаг, указывающий на необходимость обрезки по границам контейнера. По умолчанию этот флаг установлен. Здесь же мы этот флаг выставляем в положение false — объект не будет обрезаться по границам контейнера, но его видимостью будет управлять класс контейнера.
В методе инициализации создаются и настраиваются все элементы управления — кнопки со стрелками и ползунок. Флаг управления перерисовкой графика объектом ползунком сбрасывается — перерисовкой будет управлять класс контейнер.
Метод инициализации цветов объекта по умолчанию:
//+------------------------------------------------------------------+ //| CScrollBarH::Инициализация цветов объекта по умолчанию | //+------------------------------------------------------------------+ void CScrollBarH::InitColors(void) { //--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrSilver); this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrSilver); this.BorderColorToDefault(); //--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента this.InitBorderColorBlocked(clrSilver); this.InitForeColorBlocked(clrSilver); }
Цвета различных состояний установлены одинаковыми, чтобы не было смены цвета элемента при взаимодействии с мышкой.
Метод, рисующий внешний вид:
//+------------------------------------------------------------------+ //| CScrollBarH::Рисует внешний вид | //+------------------------------------------------------------------+ void CScrollBarH::Draw(const bool chart_redraw) { //--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона this.Fill(this.BackColor(),false); this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); this.m_background.Update(false); //--- Обновляем канвас фона без перерисовки графика this.m_background.Update(false); //--- Рисуем элементы списка без перерисовки графика for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Draw(false); } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Метод, возвращающий длину трека:
//+------------------------------------------------------------------+ //| CScrollBarH::Возвращает длину трека | //+------------------------------------------------------------------+ int CScrollBarH::TrackLength(void) const { if(this.m_butt_left==NULL || this.m_butt_right==NULL) return 0; return(this.m_butt_right.X()-this.m_butt_left.Right()); }
Возвращает дистанцию в пикселях между координатой X правой кнопки и правым краем левой кнопки.
Метод, возвращающий начало трека:
//+------------------------------------------------------------------+ //| CScrollBarH::Возвращает начало трека | //+------------------------------------------------------------------+ int CScrollBarH::TrackBegin(void) const { return(this.m_butt_left!=NULL ? this.m_butt_left.Width() : 0); }
Возвращает смещение на ширину кнопки относительно левого края элемента.
Метод, возвращающий позицию ползунка:
//+------------------------------------------------------------------+ //| CScrollBarH::Возвращает позицию ползунка | //+------------------------------------------------------------------+ int CScrollBarH::ThumbPosition(void) const { return(this.m_thumb!=NULL ? this.m_thumb.X()-this.TrackBegin()-this.X() : 0); }
Возвращает смещение ползунка относительно начала трека.
Метод, изменяющий ширину объекта:
//+------------------------------------------------------------------+ //| CScrollBarH::Изменяет ширину объекта | //+------------------------------------------------------------------+ bool CScrollBarH::ResizeW(const int size) { //--- Получаем указатели на левую и правую кнопки if(this.m_butt_left==NULL || this.m_butt_right==NULL) return false; //--- Изменяем ширину объекта if(!CCanvasBase::ResizeW(size)) return false; //--- Смещаем кнопки на новое расположение относительно левой и правой границ изменившего размер элемента if(!this.m_butt_left.MoveX(this.X())) return false; return(this.m_butt_right.MoveX(this.Right()-this.m_butt_right.Width()+1)); }
Горизонтальный скроллбар может изменять свой размер только в ширину. После изменения размера элемента, кнопки нужно сместить на новое место их расположения так, чтобы они были расположены по краям элемента.
Обработчик прокрутки колёсика:
//+------------------------------------------------------------------+ //| CScrollBarH::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Вызываем обработчик прокрутки для ползунка if(this.m_thumb!=NULL) this.m_thumb.OnWheelEvent(id,this.ThumbPosition(),dparam,this.NameFG()); //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id,CHARTEVENT_MOUSE_WHEEL,this.ThumbPosition(),dparam,this.NameFG()); }
Для того, чтобы ползунок скроллбара мог смещаться при прокрутке колёсика при нахождении курсора между кнопками и ползунком, метод делегирует обработку события в объект ползунка и отправляет событие прокрутки на график.
Все остальные обработчики вызываются при нахождении курсора над элементами скроллбара — над кнопками и над ползунком. В этих объектах обработчики событий уже есть.
Класс вертикальной полосы прокрутки
Класс вертикальной полосы прокрутки идентичен вышерассмотренному классу горизонтального скроллбара. Разница только в расчётах длины и начала трека, и положения ползунка, а также в изменении размеров — здесь размер меняется только по вертикали. Рассмотрим класс целиком:
//+------------------------------------------------------------------+ //| Класс вертикальной полосы прокрутки | //+------------------------------------------------------------------+ class CScrollBarV : public CPanel { protected: CButtonArrowUp *m_butt_up; // Кнопка со стрелкой вверх CButtonArrowDown *m_butt_down; // Кнопка со стрелкой вниз CScrollBarThumbV *m_thumb; // Ползунок скроллбара public: //--- Возвращает указатель на (1) левую, (2) правую кнопку, (3) ползунок CButtonArrowUp *GetButtonUp(void) { return this.m_butt_up; } CButtonArrowDown *GetButtonDown(void) { return this.m_butt_down; } CScrollBarThumbV *GetThumb(void) { return this.m_thumb; } //--- (1) Устанавливает, (2) возвращает флаг обновления графика void SetChartRedrawFlag(const bool flag) { if(this.m_thumb!=NULL) this.m_thumb.SetChartRedrawFlag(flag); } bool ChartRedrawFlag(void) const { return(this.m_thumb!=NULL ? this.m_thumb.ChartRedrawFlag() : false); } //--- Возвращает (1) длину (2) начало трека, (3) позицию ползунка int TrackLength(void) const; int TrackBegin(void) const; int ThumbPosition(void) const; //--- Изменяет размер ползунка bool SetThumbSize(const uint size) const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeH(size) : false); } //--- Изменяет высоту объекта virtual bool ResizeH(const int size); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_SCROLLBAR_V); } //--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию void Init(void); virtual void InitColors(void); //--- Обработчик прокрутки колёсика (Wheel) virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Конструкторы/деструктор CScrollBarV(void); CScrollBarV(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CScrollBarV(void) {} }; //+------------------------------------------------------------------+ //| CScrollBarV::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CScrollBarV::CScrollBarV(void) : CPanel("ScrollBarV","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H),m_butt_up(NULL),m_butt_down(NULL),m_thumb(NULL) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CScrollBarV::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CScrollBarV::CScrollBarV(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_butt_up(NULL),m_butt_down(NULL),m_thumb(NULL) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CScrollBarV::Инициализация | //+------------------------------------------------------------------+ void CScrollBarV::Init(void) { //--- Инициализация родительского класса CPanel::Init(); //--- Фон - непрозрачный this.SetAlphaBG(255); //--- Ширина рамки и текст this.SetBorderWidth(0); this.SetText(""); //--- Элемент не обрезается по границам контейнера this.m_trim_flag=false; //--- Создаём кнопки прокрутки int w=this.Width(); int h=this.Width(); this.m_butt_up = this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_UP, "","ButtU",0,0,w,h); this.m_butt_down= this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_DOWN,"","ButtD",0,this.Height()-w,w,h); if(this.m_butt_up==NULL || this.m_butt_down==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Настраиваем цвета и вид кнопки со стрелкой вверх this.m_butt_up.SetImageBound(1,0,w-4,h-2); this.m_butt_up.InitBackColors(this.m_butt_up.BackColorFocused()); this.m_butt_up.ColorsToDefault(); this.m_butt_up.InitBorderColors(this.BorderColor(),this.m_butt_up.BackColorFocused(),this.m_butt_up.BackColorPressed(),this.m_butt_up.BackColorBlocked()); this.m_butt_up.ColorsToDefault(); //--- Настраиваем цвета и вид кнопки со стрелкой вниз this.m_butt_down.SetImageBound(1,0,w-4,h-2); this.m_butt_down.InitBackColors(this.m_butt_down.BackColorFocused()); this.m_butt_down.ColorsToDefault(); this.m_butt_down.InitBorderColors(this.BorderColor(),this.m_butt_down.BackColorFocused(),this.m_butt_down.BackColorPressed(),this.m_butt_down.BackColorBlocked()); this.m_butt_down.ColorsToDefault(); //--- Создаём ползунок int tsz=this.Height()-w*2; this.m_thumb=this.InsertNewElement(ELEMENT_TYPE_SCROLLBAR_THUMB_V,"","ThumbV",1,w,w-2,tsz/2); if(this.m_thumb==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Настраиваем цвета ползунка и устанавливаем ему флаг перемещаемости this.m_thumb.InitBackColors(this.m_thumb.BackColorFocused()); this.m_thumb.ColorsToDefault(); this.m_thumb.InitBorderColors(this.m_thumb.BackColor(),this.m_thumb.BackColorFocused(),this.m_thumb.BackColorPressed(),this.m_thumb.BackColorBlocked()); this.m_thumb.ColorsToDefault(); this.m_thumb.SetMovable(true); //--- запрещаем самостоятельную перерисовку графика this.m_thumb.SetChartRedrawFlag(false); } //+------------------------------------------------------------------+ //| CScrollBarV::Инициализация цветов объекта по умолчанию | //+------------------------------------------------------------------+ void CScrollBarV::InitColors(void) { //--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrSilver); this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrSilver); this.BorderColorToDefault(); //--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента this.InitBorderColorBlocked(clrSilver); this.InitForeColorBlocked(clrSilver); } //+------------------------------------------------------------------+ //| CScrollBarV::Рисует внешний вид | //+------------------------------------------------------------------+ void CScrollBarV::Draw(const bool chart_redraw) { //--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона this.Fill(this.BackColor(),false); this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); this.m_background.Update(false); //--- Обновляем канвас фона без перерисовки графика this.m_background.Update(false); //--- Рисуем элементы списка без перерисовки графика for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.Draw(false); } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| CScrollBarV::Возвращает длину трека | //+------------------------------------------------------------------+ int CScrollBarV::TrackLength(void) const { if(this.m_butt_up==NULL || this.m_butt_down==NULL) return 0; return(this.m_butt_down.Y()-this.m_butt_up.Bottom()); } //+------------------------------------------------------------------+ //| CScrollBarV::Возвращает начало ползунка | //+------------------------------------------------------------------+ int CScrollBarV::TrackBegin(void) const { return(this.m_butt_up!=NULL ? this.m_butt_up.Height() : 0); } //+------------------------------------------------------------------+ //| CScrollBarV::Возвращает позицию ползунка | //+------------------------------------------------------------------+ int CScrollBarV::ThumbPosition(void) const { return(this.m_thumb!=NULL ? this.m_thumb.Y()-this.TrackBegin()-this.Y() : 0); } //+------------------------------------------------------------------+ //| CScrollBarV::Изменяет высоту объекта | //+------------------------------------------------------------------+ bool CScrollBarV::ResizeH(const int size) { //--- Получаем указатели на верхнюю и нижнюю кнопки if(this.m_butt_up==NULL || this.m_butt_down==NULL) return false; //--- Изменяем высоту объекта if(!CCanvasBase::ResizeH(size)) return false; //--- Смещаем кнопки на новое расположение относительно верхней и нижней границ изменившего размер элемента if(!this.m_butt_up.MoveY(this.Y())) return false; return(this.m_butt_down.MoveY(this.Bottom()-this.m_butt_down.Height()+1)); } //+------------------------------------------------------------------+ //| CScrollBarV::Обработчик прокрутки колёсика | //+------------------------------------------------------------------+ void CScrollBarV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Вызываем обработчик прокрутки для ползунка if(this.m_thumb!=NULL) this.m_thumb.OnWheelEvent(id,this.ThumbPosition(),dparam,this.NameFG()); //--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam ::EventChartCustom(this.m_chart_id,CHARTEVENT_MOUSE_WHEEL,this.ThumbPosition(),dparam,this.NameFG()); }
Итак, у нас всё готово для создания графического элемента Контейнер. В отличие от Панели и Группы Элементов, в контейнер можно разместить только один элемент управления, например, Панель. Тогда контейнер будет прокручивать полосами прокрутки только панель, а размещённые на панели различные элементы управления будут смещаться вместе с ней, корректно обрезаясь по границам видимой области контейнера. Видимая область задаётся четырьмя величинами — шириной рамки сверху, снизу, слева и справа.
CContainer — это универсальный контейнер для пользовательских интерфейсов, предназначенный для размещения одного крупного элемента с возможностью автоматической прокрутки содержимого по горизонтали и/или вертикали. Класс реализует логику появления и управления полосами прокрутки (скроллбарами) в зависимости от размеров вложенного элемента относительно видимой области контейнера.
Класс "Контейнер"
Продолжим писать код в файле Controls.mqh:
//+------------------------------------------------------------------+ //| Класс Контейнер | //+------------------------------------------------------------------+ class CContainer : public CPanel { private: bool m_visible_scrollbar_h; // Флаг видимости горизонтальной полосы прокрутки bool m_visible_scrollbar_v; // Флаг видимости вертикальной полосы прокрутки //--- Возвращает тип элемента, отправившего событие ENUM_ELEMENT_TYPE GetEventElementType(const string name); protected: CScrollBarH *m_scrollbar_h; // Указатель на горизонтальную полосу прокрутки CScrollBarV *m_scrollbar_v; // Указатель на вертикальную полосу прокрутки //--- Проверяет размеры элемента для отображения полос прокрутки void CheckElementSizes(CElementBase *element); //--- Рассчитывает и возвращает размер (1) ползунка, (2) полный, (3) рабочий размер трека горизонтального скроллбара int ThumbSizeHor(void); int TrackLengthHor(void) const { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.TrackLength() : 0); } int TrackEffectiveLengthHor(void) { return(this.TrackLengthHor()-this.ThumbSizeHor()); } //--- Рассчитывает и возвращает размер (1) ползунка, (2) полный, (3) рабочий размер трека вертикального скроллбара int ThumbSizeVer(void); int TrackLengthVer(void) const { return(this.m_scrollbar_v!=NULL ? this.m_scrollbar_v.TrackLength() : 0); } int TrackEffectiveLengthVer(void) { return(this.TrackLengthVer()-this.ThumbSizeVer()); } //--- Размер видимой области содержимого по (1) горизонтали, (2) вертикали int ContentVisibleHor(void) const { return int(this.Width()-this.BorderWidthLeft()-this.BorderWidthRight()); } int ContentVisibleVer(void) const { return int(this.Height()-this.BorderWidthTop()-this.BorderWidthBottom()); } //--- Полный размер содержимого по (1) горизонтали, (2) вертикали int ContentSizeHor(void); int ContentSizeVer(void); //--- Позиция содержимого по (1) горизонтали, (2) вертикали int ContentPositionHor(void); int ContentPositionVer(void); //--- Рассчитывает и возвращает величину смещения содержимого по (1) горизонтали, (2) вертикали в зависимости от положения ползунка int CalculateContentOffsetHor(const uint thumb_position); int CalculateContentOffsetVer(const uint thumb_position); //--- Рассчитывает и возвращает величину смещения ползунка по (1) горизонтали, (2) вертикали в зависимости от положения контента int CalculateThumbOffsetHor(const uint content_position); int CalculateThumbOffsetVer(const uint content_position); //--- Смещает содержимое по (1) горизонтали, (2) вертикали на указанное значение bool ContentShiftHor(const int value); bool ContentShiftVer(const int value); public: //--- Возврат указателей на скроллбары, кнопки и ползунки скроллбаров CScrollBarH *GetScrollBarH(void) { return this.m_scrollbar_h; } CScrollBarV *GetScrollBarV(void) { return this.m_scrollbar_v; } CButtonArrowUp *GetScrollBarButtonUp(void) { return(this.m_scrollbar_v!=NULL ? this.m_scrollbar_v.GetButtonUp() : NULL); } CButtonArrowDown *GetScrollBarButtonDown(void) { return(this.m_scrollbar_v!=NULL ? this.m_scrollbar_v.GetButtonDown() : NULL); } CButtonArrowLeft *GetScrollBarButtonLeft(void) { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.GetButtonLeft() : NULL); } CButtonArrowRight*GetScrollBarButtonRight(void) { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.GetButtonRight(): NULL); } CScrollBarThumbH *GetScrollBarThumbH(void) { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.GetThumb() : NULL); } CScrollBarThumbV *GetScrollBarThumbV(void) { return(this.m_scrollbar_v!=NULL ? this.m_scrollbar_v.GetThumb() : NULL); } //--- Устанавливает флаг прокрутки содержимого void SetScrolling(const bool flag) { this.m_scroll_flag=flag; } //--- Возвращает флаг видимости (1) горизонтального, (2) вертикального скроллбара bool ScrollBarHorIsVisible(void) const { return this.m_visible_scrollbar_h; } bool ScrollBarVerIsVisible(void) const { return this.m_visible_scrollbar_v; } //--- Создаёт и добавляет (1) новый, (2) ранее созданный элемент в список virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h); virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy); //--- Рисует внешний вид virtual void Draw(const bool chart_redraw); //--- Тип объекта virtual int Type(void) const { return(ELEMENT_TYPE_CONTAINER); } //--- Обработчики пользовательских событий элемента при наведении курсора, щелчке и прокрутке колёсика в области объекта virtual void MouseMoveHandler(const int id, const long lparam, const double dparam, const string sparam); virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam); virtual void MouseWheelHandler(const int id, const long lparam, const double dparam, const string sparam); //--- Инициализация объекта класса void Init(void); //--- Конструкторы/деструктор CContainer(void); CContainer(const string object_name, const string text, const int x, const int y, const int w, const int h); CContainer(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h); CContainer(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CContainer (void) {} };
При создании контейнера сразу же создаются и полосы прокрутки. Изначально они скрыты, и появиться могут в случае, если размер вложенного в контейнер элемента превышает ширину и/или высоту видимой области контейнера. После появления скроллбаров автоматически становится доступным управление положением содержимого контейнера при помощи полос прокрутки.
В конструкторах класса, в списке инициализации в конструктор родительского класса передаются значения формальных параметров конструктора, а затем вызывается метод инициализации класса:
//+------------------------------------------------------------------+ //| CContainer::Конструктор по умолчанию. | //| Строит элемент в главном окне текущего графика | //| в координатах 0,0 с размерами по умолчанию | //+------------------------------------------------------------------+ CContainer::CContainer(void) : CPanel("Container","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H), m_visible_scrollbar_h(false), m_visible_scrollbar_v(false) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CContainer::Конструктор параметрический. | //| Строит элемент в главном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CContainer::CContainer(const string object_name,const string text,const int x,const int y,const int w,const int h) : CPanel(object_name,text,::ChartID(),0,x,y,w,h), m_visible_scrollbar_h(false), m_visible_scrollbar_v(false) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CContainer::Конструктор параметрический. | //| Строит элемент в указанном окне текущего графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CContainer::CContainer(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,::ChartID(),wnd,x,y,w,h), m_visible_scrollbar_h(false), m_visible_scrollbar_v(false) { //--- Инициализация this.Init(); } //+------------------------------------------------------------------+ //| CContainer::Конструктор параметрический. | //| Строит элемент в указанном окне указанного графика | //| с указанными текстом, координами и размерами | //+------------------------------------------------------------------+ CContainer::CContainer(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h), m_visible_scrollbar_h(false), m_visible_scrollbar_v(false) { //--- Инициализация this.Init(); }
Метод инициализации класса:
//+------------------------------------------------------------------+ //| CContainer::Инициализация | //+------------------------------------------------------------------+ void CContainer::Init(void) { //--- Инициализация родительского объекта CPanel::Init(); //--- Ширина рамки this.SetBorderWidth(0); //--- Создаём горизонтальный скроллбар this.m_scrollbar_h=dynamic_cast<CScrollBarH *>(CPanel::InsertNewElement(ELEMENT_TYPE_SCROLLBAR_H,"","ScrollBarH",0,this.Height()-DEF_SCROLLBAR_TH-1,this.Width()-1,DEF_SCROLLBAR_TH)); if(m_scrollbar_h!=NULL) { //--- Скрываем элемент и устанавливаем запрет самостоятельной перерисовки графика this.m_scrollbar_h.Hide(false); this.m_scrollbar_h.SetChartRedrawFlag(false); } //--- Создаём вертикальный скроллбар this.m_scrollbar_v=dynamic_cast<CScrollBarV *>(CPanel::InsertNewElement(ELEMENT_TYPE_SCROLLBAR_V,"","ScrollBarV",this.Width()-DEF_SCROLLBAR_TH-1,0,DEF_SCROLLBAR_TH,this.Height()-1)); if(m_scrollbar_v!=NULL) { //--- Скрываем элемент и устанавливаем запрет самостоятельной перерисовки графика this.m_scrollbar_v.Hide(false); this.m_scrollbar_v.SetChartRedrawFlag(false); } //--- Разрешаем прокрутку содержимого this.m_scroll_flag=true; }
Сначала инициализируем объект при помощи метода инициализации родительского класса, затем создаём два скрытых скроллбара и устанавливаем флаг разрешения прокрутки содержимого контейнера.
Метод рисования:
//+------------------------------------------------------------------+ //| CContainer::Рисует внешний вид | //+------------------------------------------------------------------+ void CContainer::Draw(const bool chart_redraw) { //--- Рисуем внешний вид CPanel::Draw(false); //--- Если прокрутка разрешена if(this.m_scroll_flag) { //--- Если оба скроллбара видимы if(this.m_visible_scrollbar_h && this.m_visible_scrollbar_v) { //--- получаем указатели на две кнопки правого нижнего угла CButtonArrowDown *butt_dn=this.GetScrollBarButtonDown(); CButtonArrowRight*butt_rt=this.GetScrollBarButtonRight(); //--- Получаем указатель на горизонтальную полосу прокрутки и берём цвет её фона CScrollBarH *scroll_bar=this.GetScrollBarH(); color clr=(scroll_bar!=NULL ? scroll_bar.BackColor() : clrWhiteSmoke); //--- Определяем размеры прямоугольника в нижнем правом углу по размерам двух кнопок int bw=(butt_rt!=NULL ? butt_rt.Width() : DEF_SCROLLBAR_TH-3); int bh=(butt_dn!=NULL ? butt_dn.Height(): DEF_SCROLLBAR_TH-3); //--- Устанавливаем координаты, в которых будет нарисован закрашенный прямоугольник int x1=this.Width()-bw-1; int y1=this.Height()-bh-1; int x2=this.Width()-3; int y2=this.Height()-3; //--- Рисуем прямоугольник цветом фона скроллбара в нижнем правом углу this.m_foreground.FillRectangle(x1,y1,x2,y2,::ColorToARGB(clr)); this.m_foreground.Update(false); } } //--- Если указано - обновляем график if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Сначала рисуем панель, затем проверяем флаги видимости скроллбаров. Если видимы оба скроллбара, то необходимо нарисовать закрашенный прямоугольник цветом фона скроллбара в правом нижнем углу в месте пересечения горизонтального и вертикального скроллбаров для создания целостности их отображения.
Метод, создающий и добавляющий новый элемент в список:
//+------------------------------------------------------------------+ //| CContainer::Создаёт и добавляет новый элемент в список | //+------------------------------------------------------------------+ CElementBase *CContainer::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h) { //--- Проверяем, чтобы в списке было не более трёх объектов - две полосы прокрутки и добавляемый if(this.m_list_elm.Total()>2) { ::PrintFormat("%s: Error. You can only add one element to a container\nTo add multiple elements, use the panel",__FUNCTION__); return NULL; } //--- Создаём и добавляем новый элемент при помощи метода родительского класса //--- Элемент помещается в координаты 0,0 независимо от указанных в параметрах CElementBase *elm=CPanel::InsertNewElement(type,text,user_name,0,0,w,h); //--- Проверяем размеры элемента для отображения полос прокрутки this.CheckElementSizes(elm); //--- Возвращаем указатель на элемент return elm; }
В контейнер нельзя добавить более одного элемента + 2 скроллбара. Все добавляемые элементы содержатся в одном списке. Два скроллбара добавляются в список при создании контейнера, и остаётся место только для одного графического элемента, который можно добавить в контейнер. Именно этот элемент и будет представлять содержимое контейнера и прокручиваться полосами прокрутки в случае, если его размеры превышают ширину и/или высоту видимой области контейнера. После добавления элемента проверяются его размеры для отображения полос прокрутки в случае, если элемент больше видимой части контейнера.
Метод, добавляющий указанный элемент в список:
//+------------------------------------------------------------------+ //| CContainer::Добавляет указанный элемент в список | //+------------------------------------------------------------------+ CElementBase *CContainer::InsertElement(CElementBase *element,const int dx,const int dy) { //--- Проверяем, чтобы в списке было не более трёх объектов - две полосы прокрутки и добавляемый if(this.m_list_elm.Total()>2) { ::PrintFormat("%s: Error. You can only add one element to a container\nTo add multiple elements, use the panel",__FUNCTION__); return NULL; } //--- Добавляем указанный элемент при помощи метода родительского класса //--- Элемент помещается в координаты 0,0 независимо от указанных в параметрах CElementBase *elm=CPanel::InsertElement(element,0,0); //--- Проверяем размеры элемента для отображения полос прокрутки this.CheckElementSizes(elm); //--- Возвращаем указатель на элемент return elm; }
Метод работает аналогично предыдущему с той лишь разницей, что в список добавляется уже созданный ранее элемент.
Метод, проверяющий размеры элемента для отображения полос прокрутки:
//+------------------------------------------------------------------+ //| CContainer::Проверяет размеры элемента | //| для отображения полос прокрутки | //+------------------------------------------------------------------+ void CContainer::CheckElementSizes(CElementBase *element) { //--- Если передан пустой элемент, или прокрутка запрещена - уходим if(element==NULL || !this.m_scroll_flag) return; //--- Получаем тип элемента и, если это скроллбар - уходим ENUM_ELEMENT_TYPE type=(ENUM_ELEMENT_TYPE)element.Type(); if(type==ELEMENT_TYPE_SCROLLBAR_H || type==ELEMENT_TYPE_SCROLLBAR_V) return; //--- Инициализируем флаги отображения полос прокрутки this.m_visible_scrollbar_h=false; this.m_visible_scrollbar_v=false; //--- Если ширина элемента больше ширины видимой области контейнера - //--- устанавливаем флаг отображения горизонтальной полосы прокрутки if(element.Width()>this.ContentVisibleHor()) this.m_visible_scrollbar_h=true; //--- Если высота элемента больше высоты видимой области контейнера - //--- устанавливаем флаг отображения вертикальной полосы прокрутки if(element.Height()>this.ContentVisibleVer()) this.m_visible_scrollbar_v=true; //--- Если обе полосы прокрутки должны быть отображены if(this.m_visible_scrollbar_h && this.m_visible_scrollbar_v) { //--- Получаем указатели на две кнопки прокрутки в нижнем правом углу CButtonArrowRight *br=this.m_scrollbar_h.GetButtonRight(); CButtonArrowDown *bd=this.m_scrollbar_v.GetButtonDown(); //--- Получаем размеры кнопок прокрутки в высоту и ширину, //--- на которые необходимо уменьшить полосы прокрутки, и int v=(bd!=NULL ? bd.Height() : DEF_SCROLLBAR_TH); int h=(br!=NULL ? br.Width() : DEF_SCROLLBAR_TH); //--- изменяем размеры обеих полос прокрутки на размер кнопок this.m_scrollbar_v.ResizeH(this.m_scrollbar_v.Height()-v); this.m_scrollbar_h.ResizeW(this.m_scrollbar_h.Width() -h); } //--- Если горизонтальная полоса прокрутки должна быть показана if(this.m_visible_scrollbar_h) { //--- Уменьшаем размер видимого окна контейнера снизу на толщину полосы прокрутки + 1 пиксель this.SetBorderWidthBottom(this.m_scrollbar_h.Height()+1); //--- Корректируем размер ползунка под новый размер полосы прокрутки и //--- переносим скроллбар на передний план, делая его при этом видимым this.m_scrollbar_h.SetThumbSize(this.ThumbSizeHor()); this.m_scrollbar_h.BringToTop(false); } //--- Если вертикальная полоса прокрутки должна быть показана if(this.m_visible_scrollbar_v) { //--- Уменьшаем размер видимого окна контейнера справа на толщину полосы прокрутки + 1 пиксель this.SetBorderWidthRight(this.m_scrollbar_v.Width()+1); //--- Корректируем размер ползунка под новый размер полосы прокрутки и //--- переносим скроллбар на передний план, делая его при этом видимым this.m_scrollbar_v.SetThumbSize(this.ThumbSizeVer()); this.m_scrollbar_v.BringToTop(false); } //--- Если любая из полос прокрутки видима - обрезаем привязанный элемент по новым размерам видимой области if(this.m_visible_scrollbar_h || this.m_visible_scrollbar_v) { CElementBase *elm=this.GetAttachedElementAt(2); if(elm!=NULL) elm.ObjectTrim(); } }
Логика метода полностью расписана в комментариях к коду. Метод вызывается только при добавлении в контейнер элемента, представляющего его содержимое.
Методы расчёта размеров ползунков скроллбаров:
//+-------------------------------------------------------------------+ //|CContainer::Рассчитывает размер ползунка горизонтального скроллбара| //+-------------------------------------------------------------------+ int CContainer::ThumbSizeHor(void) { CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL || elm.Width()==0 || this.TrackLengthHor()==0) return 0; return int(::round(::fmax(((double)this.ContentVisibleHor() / (double)elm.Width()) * (double)this.TrackLengthHor(), DEF_THUMB_MIN_SIZE))); } //+------------------------------------------------------------------+ //| CContainer::Рассчитывает размер ползунка вертикального скроллбара| //+------------------------------------------------------------------+ int CContainer::ThumbSizeVer(void) { CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL || elm.Height()==0 || this.TrackLengthVer()==0) return 0; return int(::round(::fmax(((double)this.ContentVisibleVer() / (double)elm.Height()) * (double)this.TrackLengthVer(), DEF_THUMB_MIN_SIZE))); }
Метод рассчитывает размер ползунка скроллбара так, чтобы он был пропорционален отношению видимой области контейнера к полному размеру (ширина/высота) содержимого. Чем больше видимая часть относительно всего содержимого, тем больше будет ползунок. Минимальный размер ограничен константой DEF_THUMB_MIN_SIZE .
- Если содержимого нет ( elm==NULL или ширина 0) или трек скроллбара нулевой длины — метод возвращает 0.
- В противном случае метод вычисляет:
(видимый размер контейнера / полный размер содержимого) * длина трека скроллбара. - Результат округляется и сравнивается с минимальным размером ползунка, чтобы он не был слишком маленьким.
Методы, возвращающие полный размер содержимого контейнера:
//+------------------------------------------------------------------+ //| CContainer::Полный размер содержимого по горизонтали | //+------------------------------------------------------------------+ int CContainer::ContentSizeHor(void) { CElementBase *elm=this.GetAttachedElementAt(2); return(elm!=NULL ? elm.Width() : 0); } //+------------------------------------------------------------------+ //| CContainer::Полный размер содержимого по вертикали | //+------------------------------------------------------------------+ int CContainer::ContentSizeVer(void) { CElementBase *elm=this.GetAttachedElementAt(2); return(elm!=NULL ? elm.Height() : 0); }
Методы возвращают ширину/высоту содержимого контейнера. Если не удалось получить содержимое — возвращается ноль.
Методы, возвращающие позицию содержимого контейнера во горизонтали/вертикали:
//+--------------------------------------------------------------------+ //|CContainer::Возвращает позицию содержимого контейнера по горизонтали| //+--------------------------------------------------------------------+ int CContainer::ContentPositionHor(void) { CElementBase *elm=this.GetAttachedElementAt(2); return(elm!=NULL ? elm.X()-this.X() : 0); } //+------------------------------------------------------------------+ //|CContainer::Возвращает позицию содержимого контейнера по вертикали| //+------------------------------------------------------------------+ int CContainer::ContentPositionVer(void) { CElementBase *elm=this.GetAttachedElementAt(2); return(elm!=NULL ? elm.Y()-this.Y() : 0); }
Методы возвращают смещение начала координат содержимого контейнера относительно начала координат контейнера. За начало координат принят левый верхний угол.
Методы, рассчитывающие и возвращающие величину смещения содержимого контейнера по положению ползунка:
//+------------------------------------------------------------------+ //| CContainer::Рассчитывает и возвращает величину смещения | //| содержимого контейнера по горизонтали по положению ползунка | //+------------------------------------------------------------------+ int CContainer::CalculateContentOffsetHor(const uint thumb_position) { CElementBase *elm=this.GetAttachedElementAt(2); int effective_track_length=this.TrackEffectiveLengthHor(); if(elm==NULL || effective_track_length==0) return 0; return (int)::round(((double)thumb_position / (double)effective_track_length) * ((double)elm.Width() - (double)this.ContentVisibleHor())); } //+------------------------------------------------------------------+ //| CContainer::Рассчитывает и возвращает величину смещения | //| содержимого контейнера по вертикали по положению ползунка | //+------------------------------------------------------------------+ int CContainer::CalculateContentOffsetVer(const uint thumb_position) { CElementBase *elm=this.GetAttachedElementAt(2); int effective_track_length=this.TrackEffectiveLengthVer(); if(elm==NULL || effective_track_length==0) return 0; return (int)::round(((double)thumb_position / (double)effective_track_length) * ((double)elm.Height() - (double)this.ContentVisibleVer())); }
Методы вычисляют, на сколько пикселей нужно сместить содержимое контейнера в зависимости от текущего положения ползунка скроллбара.
- Определяется эффективная длина трека скроллбара (длина трека минус размер ползунка).
- Если содержимого нет или трек нулевой длины — возвращается 0.
- Смещение содержимого вычисляется пропорционально положению ползунка:
- Горизонтальный скроллбар:
(позиция ползунка / длина трека) * (общая ширина содержимого — ширина видимой области) - Вертикальный скроллбар:
(позиция ползунка / длина трека) * (общая высота содержимого — высота видимой области)
- Горизонтальный скроллбар:
- Результат округляется до целого.
Методы обеспечивают синхронизацию положения ползунка и прокрутки содержимого: когда пользователь двигает ползунок, содержимое прокручивается на соответствующее расстояние.
Методы, рассчитывающие и возвращающие величину смещения ползунка в зависимости от положения контента:
//+------------------------------------------------------------------+ //| CContainer::Рассчитывает и возвращает величину смещения ползунка | //| по горизонтали в зависимости от положения контента | //+------------------------------------------------------------------+ int CContainer::CalculateThumbOffsetHor(const uint content_position) { CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL) return 0; int value=elm.Width()-this.ContentVisibleHor(); if(value==0) return 0; return (int)::round(((double)content_position / (double)value) * (double)this.TrackEffectiveLengthHor()); } //+------------------------------------------------------------------+ //| CContainer::Рассчитывает и возвращает величину смещения ползунка | //| по вертикали в зависимости от положения контента | //+------------------------------------------------------------------+ int CContainer::CalculateThumbOffsetVer(const uint content_position) { CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL) return 0; int value=elm.Height()-this.ContentVisibleVer(); if(value==0) return 0; return (int)::round(((double)content_position / (double)value) * (double)this.TrackEffectiveLengthVer()); }
Методы вычисляют положение ползунка скроллбара (по горизонтали или вертикали) в зависимости от текущего смещения содержимого контейнера.
- Определяется максимально-возможное смещение содержимого (размер содержимого минус размер видимой области).
- Если содержимого нет или оно полностью помещается в контейнер — возвращается 0.
- Положение ползунка вычисляется пропорционально смещению содержимого:
- (смещение содержимого / максимальное смещение) * длина трека скроллбара
- Результат округляется до целого.
Методы обеспечивают синхронизацию: когда содержимое прокручивается программно или вручную, ползунок скроллбара автоматически занимает соответствующее положение на треке.
Методы, смещающие содержимое контейнера на указанное значение:
//+-------------------------------------------------------------------+ //|CContainer::Смещает содержимое по горизонтали на указанное значение| //+-------------------------------------------------------------------+ bool CContainer::ContentShiftHor(const int value) { //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL) return false; //--- Рассчитываем величину смещения по положению ползунка int content_offset=this.CalculateContentOffsetHor(value); //--- Возвращаем результат сдвига содержимого на рассчитанную величину return(elm.MoveX(this.X()-content_offset)); } //+------------------------------------------------------------------+ //| CContainer::Смещает содержимое по вертикали на указанное значение| //+------------------------------------------------------------------+ bool CContainer::ContentShiftVer(const int value) { //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElementAt(2); if(elm==NULL) return false; //--- Рассчитываем величину смещения по положению ползунка int content_offset=this.CalculateContentOffsetVer(value); //--- Возвращаем результат сдвига содержимого на рассчитанную величину return(elm.MoveY(this.Y()-content_offset)); }
Получаем указатель на содержимое контейнера, по положению ползунка рассчитываем смещение содержимого и возвращаем результат смещения содержимого контейнера на полученную величину.
Метод, возвращающий тип элемента скроллбара, отправившего событие:
//+------------------------------------------------------------------+ //| Возвращает тип элемента, отправившего событие | //+------------------------------------------------------------------+ ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name) { //--- Получаем имена всех элементов в иерархии (при ошибке - возвращаем -1) string names[]={}; int total = GetElementNames(name,"_",names); if(total==WRONG_VALUE) return WRONG_VALUE; //--- Если имя базового элемента в иерархии не совпадает с именем контейнера, то это не наше событие - уходим string base_name=names[0]; if(base_name!=this.NameFG()) return WRONG_VALUE; //--- События, пришедшие не от скроллбаров, пропускаем string check_name=::StringSubstr(names[1],0,4); if(check_name!="SCBH" && check_name!="SCBV") return WRONG_VALUE; //--- Получаем имя элемента, от которого пришло событие и инициализируем тип элемента string elm_name=names[names.Size()-1]; ENUM_ELEMENT_TYPE type=WRONG_VALUE; //--- Проверяем и записываем тип элемента //--- Кнопка со стрелкой вверх if(::StringFind(elm_name,"BTARU")==0) type=ELEMENT_TYPE_BUTTON_ARROW_UP; //--- Кнопка со стрелкой вниз else if(::StringFind(elm_name,"BTARD")==0) type=ELEMENT_TYPE_BUTTON_ARROW_DOWN; //--- Кнопка со стрелкой влево else if(::StringFind(elm_name,"BTARL")==0) type=ELEMENT_TYPE_BUTTON_ARROW_LEFT; //--- Кнопка со стрелкой вправо else if(::StringFind(elm_name,"BTARR")==0) type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT; //--- Ползунок горизонтальной полосы прокрутки else if(::StringFind(elm_name,"THMBH")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_H; //--- Ползунок вертикальной полосы прокрутки else if(::StringFind(elm_name,"THMBV")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_V; //--- Элемент управления ScrollBarHorisontal else if(::StringFind(elm_name,"SCBH")==0) type=ELEMENT_TYPE_SCROLLBAR_H; //--- Элемент управления ScrollBarVertical else if(::StringFind(elm_name,"SCBV")==0) type=ELEMENT_TYPE_SCROLLBAR_V; //--- Возвращаем тип элемента return type; }
Метод определяет тип элемента (например, кнопка скроллбара, ползунок и т.д.), который отправил событие, по имени этого элемента.
-
Имя элемента разбивается на части по символу "_", чтобы получить иерархию вложенности объектов.
-
Проверяется, что базовое имя (первый элемент в иерархии) совпадает с именем текущего контейнера. Если нет — событие не относится к этому контейнеру, возвращается WRONG_VALUE .
-
Далее проверяется, что второй элемент в иерархии — это скроллбар ( SCBH или SCBV ). Если нет — событие игнорируется.
-
По последней части имени (имени самого элемента) определяется тип элемента:
- BTARU — кнопка со стрелкой вверх
- BTARD — кнопка вниз
- BTARL — кнопка влево
- BTARR — кнопка вправо
- THMBH — горизонтальный ползунок
- THMBV — вертикальный ползунок
- SCBH — горизонтальный скроллбар
- SCBV — вертикальный скроллбар
-
Возвращается соответствующий тип элемента ( ENUM_ELEMENT_TYPE ). Если тип не определён — возвращается WRONG_VALUE .
Метод позволяет контейнеру быстро и надёжно понять, какой именно элемент скроллбара вызвал событие, чтобы правильно обработать его (например, прокрутить содержимое или сдвинуть ползунок).
Обработчик пользовательского события элемента при перемещении курсора в области объекта:
//+------------------------------------------------------------------+ //| CContainer::Обработчик пользовательского события элемента | //| при перемещении курсора в области объекта | //+------------------------------------------------------------------+ void CContainer::MouseMoveHandler(const int id,const long lparam,const double dparam,const string sparam) { bool res=false; //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElementAt(2); //--- Получаем тип элемента, от которого пришло событие ENUM_ELEMENT_TYPE type=this.GetEventElementType(sparam); //--- Если не удалось получить тип элемента или указатель на содержимое - уходим if(type==WRONG_VALUE || elm==NULL) return; //--- Если событие ползунка горизонтального скроллбара - сдвигаем содержимое по горизонтали if(type==ELEMENT_TYPE_SCROLLBAR_THUMB_H) res=this.ContentShiftHor((int)lparam); //--- Если событие ползунка вертикального скроллбара - сдвигаем содержимое по вертикали if(type==ELEMENT_TYPE_SCROLLBAR_THUMB_V) res=this.ContentShiftVer((int)lparam); //--- Если содержимое успешно сдвинуто - обновляем график if(res) ::ChartRedraw(this.m_chart_id); }
Определяем тип события и, если событие пришло от полосы прокрутки, вызываем метод смещения содержимого контейнера в соответствии с типом скроллбара — вертикальный, или горизонтальный.
Обработчик пользовательского события элемента при щелчке в области объекта:
//+------------------------------------------------------------------+ //| CContainer::Обработчик пользовательского события элемента | //| при щелчке в области объекта | //+------------------------------------------------------------------+ void CContainer::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam) { bool res=false; //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElementAt(2); //--- Получаем тип элемента, от которого пришло событие ENUM_ELEMENT_TYPE type=this.GetEventElementType(sparam); //--- Если не удалось получить тип элемента или указатель на содержимое - уходим if(type==WRONG_VALUE || elm==NULL) return; //--- Если события кнопок горизонтального скроллбара, if(type==ELEMENT_TYPE_BUTTON_ARROW_LEFT || type==ELEMENT_TYPE_BUTTON_ARROW_RIGHT) { //--- Проверяем указатель на горизонтальный скроллбар if(this.m_scrollbar_h==NULL) return; //--- получаем указатель на ползунок скроллбара CScrollBarThumbH *obj=this.m_scrollbar_h.GetThumb(); if(obj==NULL) return; //--- определяем направление смещения ползунка по типу нажатой кнопки int direction=(type==ELEMENT_TYPE_BUTTON_ARROW_LEFT ? 120 : -120); //--- вызываем обработчик прокрутки объекта ползунка для смещения ползунка в направлении direction obj.OnWheelEvent(id,0,direction,this.NameFG()); //--- Успешно res=true; } //--- Если события кнопок вертикального скроллбара, if(type==ELEMENT_TYPE_BUTTON_ARROW_UP || type==ELEMENT_TYPE_BUTTON_ARROW_DOWN) { //--- Проверяем указатель на вертикальный скроллбар if(this.m_scrollbar_v==NULL) return; //--- получаем указатель на ползунок скроллбара CScrollBarThumbV *obj=this.m_scrollbar_v.GetThumb(); if(obj==NULL) return; //--- определяем направление смещения ползунка по типу нажатой кнопки int direction=(type==ELEMENT_TYPE_BUTTON_ARROW_UP ? 120 : -120); //--- вызываем обработчик прокрутки объекта ползунка для смещения ползунка в направлении direction obj.OnWheelEvent(id,0,direction,this.NameFG()); //--- Успешно res=true; } //--- Если событие щелчка по горизонтальному скроллбару (между ползунком и кнопками прокрутки), if(type==ELEMENT_TYPE_SCROLLBAR_H) { //--- Проверяем указатель на горизонтальный скроллбар if(this.m_scrollbar_h==NULL) return; //--- получаем указатель на ползунок скроллбара CScrollBarThumbH *thumb=this.m_scrollbar_h.GetThumb(); if(thumb==NULL) return; //--- Направление смещения ползунка int direction=(lparam>=thumb.Right() ? 1 : lparam<=thumb.X() ? -1 : 0); //--- Проверяем делитель на нулевое значение if(this.ContentSizeHor()-this.ContentVisibleHor()==0) return; //--- Рассчитываем смещение ползунка, пропорциональное смещению содержимого на один экран int thumb_shift=(int)::round(direction * ((double)this.ContentVisibleHor() / double(this.ContentSizeHor()-this.ContentVisibleHor())) * (double)this.TrackEffectiveLengthHor()); //--- вызываем обработчик прокрутки объекта ползунка для смещения ползунка в направлении смещения thumb.OnWheelEvent(id,thumb_shift,0,this.NameFG()); //--- Записываем результат смещения содержимого контейнера res=this.ContentShiftHor(thumb_shift); } //--- Если событие щелчка по вертикальному скроллбару (между ползунком и кнопками прокрутки), if(type==ELEMENT_TYPE_SCROLLBAR_V) { //--- Проверяем указатель на вертикальный скроллбар if(this.m_scrollbar_v==NULL) return; //--- получаем указатель на ползунок скроллбара CScrollBarThumbV *thumb=this.m_scrollbar_v.GetThumb(); if(thumb==NULL) return; //--- Направление смещения ползунка int cursor=int(dparam-this.m_wnd_y); int direction=(cursor>=thumb.Bottom() ? 1 : cursor<=thumb.Y() ? -1 : 0); //--- Проверяем делитель на нулевое значение if(this.ContentSizeVer()-this.ContentVisibleVer()==0) return; //--- Рассчитываем смещение ползунка, пропорциональное смещению содержимого на один экран int thumb_shift=(int)::round(direction * ((double)this.ContentVisibleVer() / double(this.ContentSizeVer()-this.ContentVisibleVer())) * (double)this.TrackEffectiveLengthVer()); //--- вызываем обработчик прокрутки объекта ползунка для смещения ползунка в направлении смещения thumb.OnWheelEvent(id,thumb_shift,0,this.NameFG()); //--- Записываем результат смещения содержимого контейнера res=this.ContentShiftVer(thumb_shift); } //--- Если всё успешно - обновляем график if(res) ::ChartRedraw(this.m_chart_id); }
Метод обрабатывает клики мышью по элементам скроллбаров (кнопки, трек, ползунки).
- При щелчке по кнопкам обработка смещения ползунка делегируется ползунку скроллбара, и в итоге ползунок смещается и прокручивается содержимое контейнера.
- При щелчке по треку (между ползунком и кнопками скроллбара) производится прокрутка содержимого на один экран. Обработка делегируется обработчику прокрутки ползунка скроллбара. В итоге содержимое контейнера прокручивается на один экран.
Метод обеспечивает стандартное поведение скроллбаров:
- Клик по стрелке — пошаговая прокрутка.
- Клик по треку — прокрутка на страницу.
- Всё синхронизировано с содержимым контейнера и положением ползунка.
Это делает работу со скроллбаром привычной и удобной для пользователя.
Обработчик пользовательского события элемента при прокрутке колёсика в области ползунка скроллбара:
//+------------------------------------------------------------------+ //| CContainer::Обработчик пользовательского события элемента | //| при прокрутке колёсика в области ползунка скроллбара | //+------------------------------------------------------------------+ void CContainer::MouseWheelHandler(const int id,const long lparam,const double dparam,const string sparam) { bool res=false; //--- Получаем указатель на содержимое контейнера CElementBase *elm=this.GetAttachedElementAt(2); //--- Получаем тип элемента, от которого пришло событие ENUM_ELEMENT_TYPE type=this.GetEventElementType(sparam); //--- Если не удалось получить указатель на содержимое, или тип элемента - уходим if(type==WRONG_VALUE || elm==NULL) return; //--- Если событие ползунка горизонтального скроллбара - сдвигаем содержимое по горизонтали if(type==ELEMENT_TYPE_SCROLLBAR_THUMB_H) res=this.ContentShiftHor((int)lparam); //--- Если событие ползунка вертикального скроллбара - сдвигаем содержимое по вертикали if(type==ELEMENT_TYPE_SCROLLBAR_THUMB_V) res=this.ContentShiftVer((int)lparam); //--- Если содержимое успешно сдвинуто - обновляем график if(res) ::ChartRedraw(this.m_chart_id); }
Метод обрабатывает событие прокрутки колесика мыши над ползунком скроллбара. В зависимости от того, какой ползунок вызвал событие — горизонтальный или вертикальный, метод смещает содержимое контейнера по горизонтали или вертикали на соответствующее расстояние. После успешного смещения содержимого график обновляется.
И на сегодня это всё, что было задумано реализовать.
Давайте проверим, что у нас получилось. Сделаем индикатор в отдельном подокне графика. Создадим графический элемент "Контейнер", внутри которого будет располагаться "Группа Элементов". В группе элементов создадим набор строк из элементов "Текстовая метка". Элемент GroupBox сделаем по размерам больше контейнера, чтобы отобразились полосы прокрутки, которые и будем тестировать.
Тестируем результат
В каталоге терминала \MQL5\Indicators\ в подпапке Tables\ создадим новый файл индикатора в подокне графика с именем iTestContainer.mq5. Подключим к нему библиотеку и объявим указатель на графический элемент Контейнер:
//+------------------------------------------------------------------+ //| iTestContainer.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include "Controls\Controls.mqh" // Библиотека элементов управления CContainer *container=NULL; // Указатель на графический элемент Container //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom deindicator initialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { } //+------------------------------------------------------------------+ //| Таймер | //+------------------------------------------------------------------+ void OnTimer(void) { }
Все элементы создадим в обработчике OnInit() индикатора:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Ищем подокно графика int wnd=ChartWindowFind(); //--- Создаём графический элемент "Контейнер" container=new CContainer("Container","",0,wnd,100,40,300,200); if(container==NULL) return INIT_FAILED; container.SetID(1); // Идентификатор container.SetAsMain(); // На графике обязательно должен быть один главный элемент container.SetBorderWidth(1); // Ширина рамки (отступ видимой области на один пиксель с каждой стороны контейнера) //--- Присоединяем к контейнеру элемент GroupBox CGroupBox *groupbox=container.InsertNewElement(ELEMENT_TYPE_GROUPBOX,"","Attached Groupbox",4,4,container.Width()*2+20,container.Height()*3+10); if(groupbox==NULL) return INIT_FAILED; groupbox.SetGroup(1); // Номер группы //--- В цикле создаёи и присоединяем к элементу GroupBox 30 строк из элементов "Текстовая метка" for(int i=0;i<30;i++) { string text=StringFormat("This is test line number %d to demonstrate how scrollbars work when scrolling the contents of the container.",(i+1)); int len=groupbox.GetForeground().TextWidth(text); CLabel *lbl=groupbox.InsertNewElement(ELEMENT_TYPE_LABEL,text,"TextString"+string(i+1),8,8+(20*i),len,20); if(lbl==NULL) return INIT_FAILED; } //--- Отрисовываем на графике все созданные элементы и распечатываем их описание в журнале container.Draw(true); container.Print(); //--- Успешно return(INIT_SUCCEEDED); }
В обработчике OnDeinit() индикатора удалим созданный Контейнер и менеджер общих ресурсов библиотеки:
//+------------------------------------------------------------------+ //| Custom deindicator initialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Удаляем элемент Контейнер и уничтожаем менеджер общих ресурсов библиотеки delete container; CCommonManager::DestroyInstance(); }
В обработчике OnChartEvent() вызовем одноимённый обработчик контейнера:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Вызываем обработчик OnChartEvent элемента Контейнер container.OnChartEvent(id,lparam,dparam,sparam); }
В обработчике OnTimer() индикатора вызовем OnTimer контейнера:
//+------------------------------------------------------------------+ //| Таймер | //+------------------------------------------------------------------+ void OnTimer(void) { //--- Вызываем обработчик OnTimer элемента Контейнер container.OnTimer(); }
Скомпилируем индикатор и запустим его на графике:
Сдвиг на полный экран при щелчке по треку работает, сдвиг при щелчке по кнопкам работает, автоповтор событий при удержании кнопок работает, прокрутка колёсиком работает.
После создания всех элементов управления в журнале будут распечатаны описания всех созданных элементов:
Container (ContainerBG, ContainerFG): ID 1, Group -1, x 100, y 40, w 300, h 200 [2]: Groupbox "Attached Groupbox" (ContainerFG_GRBX2BG, ContainerFG_GRBX2FG): ID 2, Group 1, x 100, y 40, w 620, h 610 [0]: Label "TextString1" (ContainerFG_GRBX2FG_LBL0BG, ContainerFG_GRBX2FG_LBL0FG): ID 0, Group 1, x 108, y 48, w 587, h 20 [1]: Label "TextString2" (ContainerFG_GRBX2FG_LBL1BG, ContainerFG_GRBX2FG_LBL1FG): ID 1, Group 1, x 108, y 68, w 587, h 20 [2]: Label "TextString3" (ContainerFG_GRBX2FG_LBL2BG, ContainerFG_GRBX2FG_LBL2FG): ID 2, Group 1, x 108, y 88, w 587, h 20 [3]: Label "TextString4" (ContainerFG_GRBX2FG_LBL3BG, ContainerFG_GRBX2FG_LBL3FG): ID 3, Group 1, x 108, y 108, w 587, h 20 [4]: Label "TextString5" (ContainerFG_GRBX2FG_LBL4BG, ContainerFG_GRBX2FG_LBL4FG): ID 4, Group 1, x 108, y 128, w 587, h 20 [5]: Label "TextString6" (ContainerFG_GRBX2FG_LBL5BG, ContainerFG_GRBX2FG_LBL5FG): ID 5, Group 1, x 108, y 148, w 587, h 20 [6]: Label "TextString7" (ContainerFG_GRBX2FG_LBL6BG, ContainerFG_GRBX2FG_LBL6FG): ID 6, Group 1, x 108, y 168, w 587, h 20 [7]: Label "TextString8" (ContainerFG_GRBX2FG_LBL7BG, ContainerFG_GRBX2FG_LBL7FG): ID 7, Group 1, x 108, y 188, w 587, h 20 [8]: Label "TextString9" (ContainerFG_GRBX2FG_LBL8BG, ContainerFG_GRBX2FG_LBL8FG): ID 8, Group 1, x 108, y 208, w 587, h 20 [9]: Label "TextString10" (ContainerFG_GRBX2FG_LBL9BG, ContainerFG_GRBX2FG_LBL9FG): ID 9, Group 1, x 108, y 228, w 594, h 20 [10]: Label "TextString11" (ContainerFG_GRBX2FG_LBL10BG, ContainerFG_GRBX2FG_LBL10FG): ID 10, Group 1, x 108, y 248, w 594, h 20 [11]: Label "TextString12" (ContainerFG_GRBX2FG_LBL11BG, ContainerFG_GRBX2FG_LBL11FG): ID 11, Group 1, x 108, y 268, w 594, h 20 [12]: Label "TextString13" (ContainerFG_GRBX2FG_LBL12BG, ContainerFG_GRBX2FG_LBL12FG): ID 12, Group 1, x 108, y 288, w 594, h 20 [13]: Label "TextString14" (ContainerFG_GRBX2FG_LBL13BG, ContainerFG_GRBX2FG_LBL13FG): ID 13, Group 1, x 108, y 308, w 594, h 20 [14]: Label "TextString15" (ContainerFG_GRBX2FG_LBL14BG, ContainerFG_GRBX2FG_LBL14FG): ID 14, Group 1, x 108, y 328, w 594, h 20 [15]: Label "TextString16" (ContainerFG_GRBX2FG_LBL15BG, ContainerFG_GRBX2FG_LBL15FG): ID 15, Group 1, x 108, y 348, w 594, h 20 [16]: Label "TextString17" (ContainerFG_GRBX2FG_LBL16BG, ContainerFG_GRBX2FG_LBL16FG): ID 16, Group 1, x 108, y 368, w 594, h 20 [17]: Label "TextString18" (ContainerFG_GRBX2FG_LBL17BG, ContainerFG_GRBX2FG_LBL17FG): ID 17, Group 1, x 108, y 388, w 594, h 20 [18]: Label "TextString19" (ContainerFG_GRBX2FG_LBL18BG, ContainerFG_GRBX2FG_LBL18FG): ID 18, Group 1, x 108, y 408, w 594, h 20 [19]: Label "TextString20" (ContainerFG_GRBX2FG_LBL19BG, ContainerFG_GRBX2FG_LBL19FG): ID 19, Group 1, x 108, y 428, w 594, h 20 [20]: Label "TextString21" (ContainerFG_GRBX2FG_LBL20BG, ContainerFG_GRBX2FG_LBL20FG): ID 20, Group 1, x 108, y 448, w 594, h 20 [21]: Label "TextString22" (ContainerFG_GRBX2FG_LBL21BG, ContainerFG_GRBX2FG_LBL21FG): ID 21, Group 1, x 108, y 468, w 594, h 20 [22]: Label "TextString23" (ContainerFG_GRBX2FG_LBL22BG, ContainerFG_GRBX2FG_LBL22FG): ID 22, Group 1, x 108, y 488, w 594, h 20 [23]: Label "TextString24" (ContainerFG_GRBX2FG_LBL23BG, ContainerFG_GRBX2FG_LBL23FG): ID 23, Group 1, x 108, y 508, w 594, h 20 [24]: Label "TextString25" (ContainerFG_GRBX2FG_LBL24BG, ContainerFG_GRBX2FG_LBL24FG): ID 24, Group 1, x 108, y 528, w 594, h 20 [25]: Label "TextString26" (ContainerFG_GRBX2FG_LBL25BG, ContainerFG_GRBX2FG_LBL25FG): ID 25, Group 1, x 108, y 548, w 594, h 20 [26]: Label "TextString27" (ContainerFG_GRBX2FG_LBL26BG, ContainerFG_GRBX2FG_LBL26FG): ID 26, Group 1, x 108, y 568, w 594, h 20 [27]: Label "TextString28" (ContainerFG_GRBX2FG_LBL27BG, ContainerFG_GRBX2FG_LBL27FG): ID 27, Group 1, x 108, y 588, w 594, h 20 [28]: Label "TextString29" (ContainerFG_GRBX2FG_LBL28BG, ContainerFG_GRBX2FG_LBL28FG): ID 28, Group 1, x 108, y 608, w 594, h 20 [29]: Label "TextString30" (ContainerFG_GRBX2FG_LBL29BG, ContainerFG_GRBX2FG_LBL29FG): ID 29, Group 1, x 108, y 628, w 594, h 20
Заявленный функционал работает правильно. Есть некоторые незначительные изъяны, но их поправим при дальнейшей разработке элемента управления TableView.
Заключение
Сегодня мы сделали достаточно объёмный и необходимый функционал в разрабатываемой библиотеке элементов управления.
Класс CContainer — это мощный и удобный инструмент для создания прокручиваемых областей в пользовательских интерфейсах. Он автоматизирует работу с полосами прокрутки, облегчает управление большим содержимым и обеспечивает привычное для пользователя взаимодействие с прокручиваемыми областями. Благодаря гибкой архитектуре и интеграции с другими элементами интерфейса, контейнер легко использовать в составе сложных графических решений.
Следующим нашим шагом будет создание заголовка, позволяющего разместить, например, список заголовков столбцов таблицы, при этом функционал будет позволять изменять размеры каждой ячейки заголовка, что автоматически будет менять размеры и столбцов таблицы.
Программы, используемые в статье:
# | Имя | Тип | Описание |
---|---|---|---|
1 | Base.mqh | Библиотека классов | Классы для создания базового объекта элементов управления |
2 | Controls.mqh | Библиотека классов | Классы элементов управления |
3 | iTestContainer.mq5 | Тестовый индикатор | Индикатор для тестирования работы с классами элементов управления |
4 | MQL5.zip | Архив | Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования