![Графические интерфейсы XI: Нарисованные элементы управления (build 14.2)](https://c.mql5.com/i/articles/overlay.png)
Графические интерфейсы XI: Нарисованные элементы управления (build 14.2)
Содержание
- Введение
- Методы для рисования элементов
- Новый дизайн графического интерфейса
- Всплывающие подсказки
- Новые идентификаторы событий
- Оптимизируем ядро библиотеки
- Приложение для теста элемента
- Заключение
Введение
О том, для чего предназначена эта библиотека, более подробно можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце каждой статьи серии приложена полная версия библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.
В обновленной версии библиотеки все ее элементы будут рисоваться на отдельных графических объектах типа OBJ_BITMAP_LABEL. Кроме того, я продолжу описывать глобальную оптимизацию кода библиотеки. Начало этого описания можно прочесть в предыдущей статье. Сейчас рассмотрим, как изменились классы, которые являются ядром библиотеки. Новая версия библиотеки стала ещё более объектно-ориентированной. Код стал более понятным для изучения. Благодаря этому пользователь сможет развивать библиотеку самостоятельно под собственные задачи.
Методы для рисования элементов
В классе CElement объявлен экземпляр класса для рисования. Его методы позволяют создать объект для рисования и удалить его. При необходимости можно получить его указатель.
class CElement : public CElementBase { protected: //--- Холст для рисования элемента CRectCanvas m_canvas; //--- public: //--- Возвращает указатель на холст элемента CRectCanvas *CanvasPointer(void) { return(::GetPointer(m_canvas)); } };
Теперь есть общий метод создания объекта (холста) для рисования внешнего вида элемента. Он расположен в базовом классе CElement и доступен во всех классах элементов библиотеки. Для создания графического объекта такого типа используется метод CElement::CreateCanvas(). В него в качестве аргументов нужно передать (1) имя, (2) координаты, (3) размеры и (4) цветовой формат. По умолчанию будет установлен формат COLOR_FORMAT_ARGB_NORMALIZE, позволяющий делать элементы прозрачными. Если переданы некорректные размеры, в начале метода они будут скорректированы. После создания и закрепления объекта за графиком, на котором находится MQL-приложение, устанавливаются базовые свойства, которые ранее многократно повторялись во всех классах элементов.
class CElement : public CElementBase { public: //--- Создание холста для рисования bool CreateCanvas(const string name,const int x,const int y, const int x_size,const int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE); }; //+------------------------------------------------------------------+ //| Создание холста для рисования элемента | //+------------------------------------------------------------------+ bool CElement::CreateCanvas(const string name,const int x,const int y, const int x_size,const int y_size,ENUM_COLOR_FORMAT clr_format=COLOR_FORMAT_ARGB_NORMALIZE) { //--- Корректировка размеров int xsize =(x_size<1)? 50 : x_size; int ysize =(y_size<1)? 20 : y_size; //--- Сбросить последнюю ошибку ::ResetLastError(); //--- Создание объекта if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,xsize,ysize,clr_format)) { ::Print(__FUNCTION__," > Не удалось создать холст для рисования элемента ("+m_class_name+"): ",::GetLastError()); return(false); } //--- Сбросить последнюю ошибку ::ResetLastError(); //--- Получим указатель на базовый класс CChartObject *chart=::GetPointer(m_canvas); //--- Прикрепить к графику if(!chart.Attach(m_chart_id,name,(int)m_subwin,(int)1)) { ::Print(__FUNCTION__," > Не удалось присоединить холст для рисования к графику: ",::GetLastError()); return(false); } //--- Свойства m_canvas.Tooltip("\n"); m_canvas.Corner(m_corner); m_canvas.Selectable(false); //--- У всех элементов, кроме формы, приоритет больше, чем у главного элемента Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)? 0 : m_main.Z_Order()+1); //--- Координаты m_canvas.X(x); m_canvas.Y(y); //--- Размеры m_canvas.XSize(x_size); m_canvas.YSize(y_size); //--- Отступы от крайней точки m_canvas.XGap(CalculateXGap(x)); m_canvas.YGap(CalculateYGap(y)); return(true); }
Перейдем к базовым методам для рисования элементов управления. Все они расположены в классе CElement и объявлены как виртуальные (virtual).
В первую очередь это рисование фона. В базовом варианте это простая заливка цветом, для которой используется метод CElement::DrawBackground(). При желании можно установить прозрачность. Для этого предназначен публичный метод CElement::Alpha(), в качестве аргумента для которого передается значение альфа-канала от 0 до 255. Нулевое значение означает полную прозрачность. Прозрачность в этой версии применяется только к заливке фона и рамке. Текст и изображения будут оставаться полностью непрозрачными и чёткими при любом установленном значении альфа-канала.
class CElement : public CElementBase { protected: //--- Значение альфа-канала (прозрачность элемента) uchar m_alpha; //--- public: //--- Значение альфа-канала (прозрачность элемента) void Alpha(const uchar value) { m_alpha=value; } uchar Alpha(void) const { return(m_alpha); } //--- protected: //--- Рисует фон virtual void DrawBackground(void); }; //+------------------------------------------------------------------+ //| Рисует фон | //+------------------------------------------------------------------+ void CElement::DrawBackground(void) { m_canvas.Erase(::ColorToARGB(m_back_color,m_alpha)); }
Часто нужно нарисовать рамку для того или иного элемента. Метод CElement::DrawBorder() рисует рамку по краям объекта для рисования. Можно для этого использовать и метод Rectangle(), который рисует прямоугольник без заливки.
class CElement : public CElementBase { protected: //--- Рисует рамку virtual void DrawBorder(void); }; //+------------------------------------------------------------------+ //| Рисует рамку | //+------------------------------------------------------------------+ void CElement::DrawBorder(void) { //--- Координаты int x1=0,y1=0; int x2=m_canvas.X_Size()-1; int y2=m_canvas.Y_Size()-1; //--- Рисуем прямоугольник без заливки m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(m_border_color,m_alpha)); }
В предыдущей статье уже рассказано о том, что для любого элемента можно установить любое количество групп изображений. Поэтому метод для рисования элемента должен уметь выводить все установленные пользователем изображения. Здесь для этого используется метод CElement::DrawImage(). Программа последовательно пройдёт по всем группам и изображениям в них, попиксельно выводя их на холст элемента. Перед циклом вывода изображения определяется, какое из них выбрано в группе в текущий момент. Познакомимся с кодом этого метода:
class CElement : public CElementBase { protected: //--- Рисует картинку virtual void DrawImage(void); }; //+------------------------------------------------------------------+ //| Рисует картинку | //+------------------------------------------------------------------+ void CElement::DrawImage(void) { //--- Количество групп uint group_total=ImagesGroupTotal(); //--- Рисуем изображения for(uint g=0; g<group_total; g++) { //--- Индекс выбранного изображения int i=SelectedImage(g); //--- Если нет изображений if(i==WRONG_VALUE) continue; //--- Координаты int x =m_images_group[g].m_x_gap; int y =m_images_group[g].m_y_gap; //--- Размеры uint height =m_images_group[g].m_image[i].Height(); uint width =m_images_group[g].m_image[i].Width(); //--- Рисуем for(uint ly=0,p=0; ly<height; ly++) { for(uint lx=0; lx<width; lx++,p++) { //--- Если нет цвета, перейти к следующему пикселю if(m_images_group[g].m_image[i].Data(p)<1) continue; //--- Получаем цвет нижнего слоя (фона ячейки) и цвет указанного пикселя картинки uint background =::ColorToARGB(m_canvas.PixelGet(x+lx,y+ly)); uint pixel_color =m_images_group[g].m_image[i].Data(p); //--- Смешиваем цвета uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- Рисуем пиксель наслаиваемого изображения m_canvas.PixelSet(x+lx,y+ly,foreground); } } } }
У многих элементов есть текстовое описание. За его вывод отвечает метод CElement::DrawText(). Несколько полей в этом методе позволяют настроить отображение текста в зависимости от состояния элемента. Доступны три состояния элемента:
- заблокирован;
- нажат;
- находится в фокусе (под курсором мыши).
Кроме этого, учитывается, включен ли режим выравнивания текста по центру элемента. Код этого метода:
class CElement : public CElementBase { protected: //--- Рисует текст virtual void DrawText(void); }; //+------------------------------------------------------------------+ //| Рисует текст | //+------------------------------------------------------------------+ void CElement::DrawText(void) { //--- Координаты int x =m_label_x_gap; int y =m_label_y_gap; //--- Определим цвет для текстовой метки color clr=clrBlack; //--- Если элемент заблокирован if(m_is_locked) clr=m_label_color_locked; else { //--- Если элемент в нажатом состоянии if(!m_is_pressed) clr=(m_mouse_focus)? m_label_color_hover : m_label_color; else { if(m_class_name=="CButton") clr=m_label_color_pressed; else clr=(m_mouse_focus)? m_label_color_hover : m_label_color_pressed; } } //--- Свойства шрифта m_canvas.FontSet(m_font,-m_font_size*10,FW_NORMAL); //--- Нарисовать текст с учётом режима выравнивания по центру if(m_is_center_text) { x =m_x_size>>1; y =m_y_size>>1; m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_CENTER|TA_VCENTER); } else m_canvas.TextOut(x,y,m_label_text,::ColorToARGB(clr),TA_LEFT); }
Все вышеописанные методы будут вызываться в общем публичном виртуальном методе CElement::Draw(). У него нет базового кода, так как в каждом элементе вызываемый набор методов для рисования будет уникальным.
class CElement : public CElementBase { public: //--- Рисует элемент virtual void Draw(void) {} };
Для пользователя библиотеки предназначен метод CElement::Update(). Он вызывается каждый раз после того, как в элемент графического интерфейса внесены программные изменения. Допустимы два варианта вызова: (1) с полной перерисовкой элемента или (2) для применения внесённых перед этим изменений (см. листинг кода ниже). Этот метод тоже объявлен как виртуальный, поскольку в некоторых классах элементов могут быть свои уникальные версии, учитывающие особенности в методах и последовательности отрисовки.
class CElement : public CElementBase { public: //--- Обновляет элемент для отображения последних изменений virtual void Update(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Обновление элемента | //+------------------------------------------------------------------+ void CElement::Update(const bool redraw=false) { //--- С перерисовкой элемента if(redraw) { Draw(); m_canvas.Update(); return; } //--- Применить m_canvas.Update(); }
Новый дизайн графического интерфейса
Так как все элементы библиотеки теперь рисуются, то появилась возможность реализовать новый дизайн для графического интерфейса. Здесь можно ничего особенного не придумывать, а воспользоваться уже готовым решением. Я взял за основу лаконичную эстетику дизайна ОС Windows 10.
Изображения для ярлыков в таких элементах, как кнопки формы для элементов управления, радиокнопки, чекбоксы, комбобоксы, пункты меню, пункты древовидных списков и т.д. сделаны как в Windows 10.
Выше мы уже говорили о том, что любому элементу теперь можно установить прозрачность. На скриншоте ниже показан пример полупрозрачного окна (CWindow). Значение альфа-канала здесь 200.
Рис. 8. Демонстрация прозрачности у формы для элементов управления.
Чтобы сделать форму прозрачной по всей области, нужен метод CWindow::TransparentOnlyCaption(). По умолчанию установлен режим, когда эффект прозрачности применяется только к заголовку.
class CWindow : public CElement { private: //--- Включает прозрачность только у заголовка bool m_transparent_only_caption; //--- public: //--- Включает режим прозрачности только у заголовка окна void TransparentOnlyCaption(const bool state) { m_transparent_only_caption=state; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CWindow::CWindow(void) : m_transparent_only_caption(true) { ... }
Ниже показан внешний вид различных типов кнопок:
Рис. 9. Демонстрация внешнего вида нескольких типов кнопок.
На следующем скриншоте продемонстрировано, как выглядят теперь чекбоксы, поля ввода, комбобокс с выпадающим списком с полосой прокрутки и числовые слайдеры. Обратите внимание, что теперь можно делать анимированные картинки. В третьем пункте статусной строки имитируется потеря связи с сервером. Его внешний вид — точная копия аналогичного элемента в статусной строке MetaTrader 5.
Рис. 10. Демонстрация внешнего вида чекбоксов, комбобоксов, слайдеров и других элементов.
Как выглядят другие элементы графического интерфейса библиотеки, можно посмотреть в тестовом MQL-приложении, прикрепленном к этой статье.
Всплывающие подсказки
В класс CElement добавлены дополнительные методы для управления отображением всплывающих подсказок в элементах управления. Теперь для любого элемента можно установить штатную всплывающую подсказку, если текст умещается в 63 символа. Используйте методы CElement::Tooltip() для установки и получения текста всплывающей подсказки.
class CElement : public CElementBase { protected: //--- Текст всплывающей подсказки string m_tooltip_text; //--- public: //--- Всплывающая подсказка void Tooltip(const string text) { m_tooltip_text=text; } string Tooltip(void) const { return(m_tooltip_text); } };
Для включения или отключения режима показа всплывающей подсказки нужно воспользоваться методом CElement::ShowTooltip().
class CElement : public CElementBase { public: //--- Режим показа всплывающей подсказки void ShowTooltip(const bool state); }; //+------------------------------------------------------------------+ //| Установка показа всплывающей подсказки | //+------------------------------------------------------------------+ void CElement::ShowTooltip(const bool state) { if(state) m_canvas.Tooltip(m_tooltip_text); else m_canvas.Tooltip("\n"); }
В каждом классе элементов есть методы для получения указателей подключенных элементов. Например, если нужно сделать всплывающие подсказки для кнопок формы, то для этого в пользовательском классе в методе создания формы нужно написать вот такие строчки кода:
... //--- Установим всплывающие подсказки m_window.GetCloseButtonPointer().Tooltip("Close"); m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand"); m_window.GetTooltipButtonPointer().Tooltip("Tooltips"); ...
Ниже продемонстрировано, как это работает. Для кнопок формы включены штатные подсказки. Для элементов, где может понадобиться описание длиннее, чем 63 символа, используется элемент CTooltip.
Рис. 11. Демонстрация двух типов всплывающих подсказок (штатных и пользовательских).
Новые идентификаторы событий
Добавлены новые идентификаторы событий. За счет этого существенно сократилось потребление ресурсов процессора. Как удалось этого достичь?
При создании большого MQL-приложения с графическим интерфейсом и множеством элементов управления важно минимизировать потребление ресурсов процессора.
Если навести курсор мыши на элемент управления, он подсвечивается. Таким образом показывается, что этот элемент доступен для взаимодействия. Но не все элементы доступны и видимы одновременно.
- Выпадающие списки и календари, контекстные меню большую часть времени скрыты. Они лишь иногда открываются, чтобы пользователь выбрал нужную функцию, дату или режим.
- Группы элементов могут быть закреплены за разными вкладками, но одновременно может быть открыта только одна.
- Если форма свёрнута, то скрыты и все ее элементы.
- Если открыто диалоговое окно, то обработка событий будет только у элементов этой формы.
Логично, что нет смысла постоянно обрабатывать весь список элементов графического интерфейса, когда доступны для использования только некоторые из них. Нужно формировать массив обработки событий только для списка открытых элементов.
Есть и элементы, результат взаимодействия с которыми затронет только их. Значит, доступным для обработки должен будет остаться только он. Перечислим эти элементы и случаи:
- Перемещение ползунка полосы прокрутки (CScroll). Для обработки надо оставить доступными только саму полосу прокрутки и элемент, частью которого она является (список, таблица, многострочное поле ввода и т.д.).
- Перемещение ползунка слайдера (CSlider). Для этого достаточно, чтобы был доступен элемент слайдер и числовое поле ввода, в котором будут отражаться изменения значений.
- Изменение ширины столбцов в таблице (CTable). Нужно оставить доступной для обработки только таблицу.
- Изменение ширины списков в элементе «Древовидный список» (CTreeView). В момент захвата смежной границы списков обработать нужно только этот элемент.
- Перемещение формы (CWindow). Все элементы, кроме формы в момент её перемещения, исключаем из обработки.
Во всех перечисленных случаях элементам нужно отправлять сообщения, которые нужно принимать и обрабатывать в ядре библиотеки. Два идентификатора событий для определения доступности элементов (ON_SET_AVAILABLE) и формирования массива элементов (ON_CHANGE_GUI) будут обрабатываться в ядре. Все идентификаторы событий находятся в файле Define.mqh:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ ... #define ON_CHANGE_GUI (28) // Графический интерфейс изменился #define ON_SET_AVAILABLE (39) // Установить доступные элементы ...
Скрываем и показываем элементы методами Show() и Hide(). Для установки доступности в класс CElement добавлено новое свойство. Его значение устанавливается с помощью виртуального публичного метода CElement::IsAvailable(). Здесь, как и в других методах, определяющих состояние элементов, переданное значение устанавливается и подключенным элементам. Относительно переданного состояния устанавливаются приоритеты на нажатие левой кнопкой мыши. Если элемент должен быть недоступен, то приоритеты будут сброшены.
class CElement : public CElementBase { protected: bool m_is_available; // доступность //--- public: //--- Признак доступного элемента virtual void IsAvailable(const bool state) { m_is_available=state; } bool IsAvailable(void) const { return(m_is_available); } }; //+------------------------------------------------------------------+ //| Доступность элемента | //+------------------------------------------------------------------+ void CElement::IsAvailable(const bool state) { //--- Выйти, если уже установлено if(state==CElementBase::IsAvailable()) return; //--- Установить CElementBase::IsAvailable(state); //--- Остальные элементы int elements_total=ElementsTotal(); for(int i=0; i<elements_total; i++) m_elements[i].IsAvailable(state); //--- Установить приоритеты на нажатие левой кнопкой мыши if(state) SetZorders(); else ResetZorders(); }
В качестве примера приведём здесь код метода CComboBox::ChangeComboBoxListState(), в котором определяется видимость выпадающего списка в элементе «Комбобокс».
Если кнопка комбобокса нажата и нужно показать список, то сразу после показа списка отправляется событие с идентификатором ON_SET_AVAILABLE. В качестве дополнительных параметров отправляются (1) идентификатор элемента и (2) признак для определения того, что именно нужно сделать обработчику этого события: восстановить все видимые элементы или сделать доступным только тот, идентификатор которого указан в событии. Для восстановления используется признак со значением 1, а для установки доступности указанного элемента - 0.
Вслед за сообщением с идентификатором события ON_SET_AVAILABLE, отправляется сообщение с идентификатором события ON_CHANGE_GUI, при обработке которого будет сформирован массив с доступными сейчас элементами.
//+------------------------------------------------------------------+ //| Изменяет текущее состояние комбобокса на противоположное | //+------------------------------------------------------------------+ void CComboBox::ChangeComboBoxListState(void) { //--- Если кнопка нажата if(m_button.IsPressed()) { //--- Показать список m_listview.Show(); //--- Отправим сообщение на определение доступных элементов ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),0,""); //--- Отправим сообщение об изменении в графическом интерфейсе ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,""); } else { //--- Скрыть список m_listview.Hide(); //--- Отправим сообщение на восстановление элементов ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),1,""); //--- Отправим сообщение об изменении в графическом интерфейсе ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,""); } }
Но, например, элементу «Вкладки» достаточно отправить на обработку только одно из описанных событий, с идентификатором ON_CHANGE_GUI. Здесь нет задачи сделать доступными определенные элементы. При переключении вкладок устанавливается состояние видимости элементам, которые закреплены за группой вкладок. Для управления видимостью групп элементов в классе CTabs используется метод CTabs::ShowTabElements(), который изменен в новой версии библиотеки. Иногда в одной из вкладок может понадобиться разместить ещё одну группу вкладок. Поэтому, если при показе элементов выделенной вкладки оказалось, что один из них имеет тип CTabs, то метод CTabs::ShowTabElements() тут же вызывается и у этого элемента. Такой подход позволяет разместить вкладки на любой уровень вложенности.
//+------------------------------------------------------------------+ //| Показывает элементы только выделенной вкладки | //+------------------------------------------------------------------+ void CTabs::ShowTabElements(void) { //--- Выйти, если вкладки скрыты if(!CElementBase::IsVisible()) return; //--- Проверка индекса выделенной вкладки CheckTabIndex(); //--- uint tabs_total=TabsTotal(); for(uint i=0; i<tabs_total; i++) { //--- Получим количество элементов присоединённых к вкладке int tab_elements_total=::ArraySize(m_tab[i].elements); //--- Если выделена эта вкладка if(i==m_selected_tab) { //--- Показать элементы вкладки for(int j=0; j<tab_elements_total; j++) { //--- Показать элементы CElement *el=m_tab[i].elements[j]; el.Reset(); //--- Если этот элемент 'Вкладки', то показать элементы открытой CTabs *tb=dynamic_cast<CTabs*>(el); if(tb!=NULL) tb.ShowTabElements(); } } //--- Скрыть элементы неактивных вкладок else { for(int j=0; j<tab_elements_total; j++) m_tab[i].elements[j].Hide(); } } //--- Отправить сообщение об этом ::EventChartCustom(m_chart_id,ON_CLICK_TAB,CElementBase::Id(),m_selected_tab,""); }
После того, как элементы выделенной вкладки показаны, отправляется сообщение о том, что графический интерфейс изменён и нужно сформировать массив доступных для обработки элементов.
//+------------------------------------------------------------------+ //| Нажатие на вкладку в группе | //+------------------------------------------------------------------+ bool CTabs::OnClickTab(const int id,const int index) { //--- Выйти, если (1) идентификаторы не совпадают или (2) элемент заблокирован if(id!=CElementBase::Id() || CElementBase::IsLocked()) return(false); //--- Выйти, если индекс не совпадает if(index!=m_tabs.SelectedButtonIndex()) return(true); //--- Сохранить индекс выделенной вкладки SelectedTab(index); //--- Перерисовать элемент Reset(); Update(true); //--- Показать элементы только выделенной вкладки ShowTabElements(); //--- Отправим сообщение об изменении в графическом интерфейсе ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0.0,""); return(true); }
В файл Defines.mqh добавлены два новых идентификатора для генерации событий:
- ON_MOUSE_FOCUS — курсор мыши зашёл в область элемента;
- ON_MOUSE_BLUR — курсор мыши вышел из области элемента.
... #define ON_MOUSE_BLUR (34) // Курсор мыши вышел из области элемента #define ON_MOUSE_FOCUS (35) // Курсор мыши зашёл в область элемента ...
Эти события генерируются только в момент пересечения границ элементов. В базовом классе элементов (CElementBase) есть метод CElementBase::CheckCrossingBorder(), который проверяет момент пересечения границ элементов курсором мыши. Дополним его генерацией описанных выше событий:
//+------------------------------------------------------------------+ //| Проверка пересечения границ элемента | //+------------------------------------------------------------------+ bool CElementBase::CheckCrossingBorder(void) { //--- Если это момент пересечения границ элемента if((MouseFocus() && !IsMouseFocus()) || (!MouseFocus() && IsMouseFocus())) { IsMouseFocus(MouseFocus()); //--- Сообщение о пересечении в элемент if(MouseFocus()) ::EventChartCustom(m_chart_id,ON_MOUSE_FOCUS,m_id,m_index,m_class_name); //--- Сообщение о пересечении из элемента else ::EventChartCustom(m_chart_id,ON_MOUSE_BLUR,m_id,m_index,m_class_name); //--- return(true); } //--- return(false); }
В текущей версии библиотеки эти события обрабатываются только в главном меню (CMenuBar). Рассмотрим, как это работает.
После того, как главное меню создано и сохранено, его пункты (CMenuItem) попадают в список хранилища как отдельные элементы. Класс CMenuItem — производный от CButton (элемент «Кнопка»). Поэтому вызов обработчика событий пункта меню начинается с вызова обработчика базового класса CButton.
//+------------------------------------------------------------------+ //| Обработчик событий | //+------------------------------------------------------------------+ void CMenuItem::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработать событие в базовом классе CButton::OnEvent(id,lparam,dparam,sparam); ... }
Отслеживание пересечения кнопки уже есть в базовом обработчике событий, его не нужно повторять в производном классе CMenuItem.
//+------------------------------------------------------------------+ //| Обработчик событий | //+------------------------------------------------------------------+ void CButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработка события перемещения курсора if(id==CHARTEVENT_MOUSE_MOVE) { //--- Перерисовать элемент, если было пересечение границ if(CheckCrossingBorder()) Update(true); //--- return; } ... }
Если курсор пересек границы внутрь области кнопки, генерируется событие с идентификатором ON_MOUSE_FOCUS. Теперь в обработчике класса CMenuBar именно по этому событию переключаются контекстные меню, когда элемент «Главное меню» активирован.
//+------------------------------------------------------------------+ //| Обработчик событий | //+------------------------------------------------------------------+ void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработка события смены фокуса на кнопках меню if(id==CHARTEVENT_CUSTOM+ON_MOUSE_FOCUS) { //--- Выйти, если (2) главное меню не активировано или (2) идентификаторы не совпадают if(!m_menubar_state || lparam!=CElementBase::Id()) return; //--- Переключить контекстное меню по активированному пункту главного меню SwitchContextMenuByFocus(); return; } ... }
Оптимизируем ядро библиотеки
Рассмотрим изменения, исправления и дополнения в классах CWndContainer и CWndEvents, которые можно назвать ядром библиотеки. Ведь именно здесь организован доступ ко всем ее элементам и обрабатываются потоки событий, которые генерируются элементами графического интерфейса.
В класс CWndContainer добавлен шаблонный метод CWndContainer::ResizeArray() для работы с массивами. Массив любого типа, переданный в этот метод, будет увеличен на один элемент, а метод вернёт индекс последнего элемента.
//+------------------------------------------------------------------+ //| Класс для хранения всех объектов интерфейса | //+------------------------------------------------------------------+ class CWndContainer { private: //--- Увеличивает массив на один элемент и возвращает последний индекс template<typename T> int ResizeArray(T &array[]); }; //+------------------------------------------------------------------+ //| Увеличивает массив на один элемент и возвращает последний индекс | //+------------------------------------------------------------------+ template<typename T> int CWndContainer::ResizeArray(T &array[]) { int size=::ArraySize(array); ::ArrayResize(array,size+1,RESERVE_SIZE_ARRAY); return(size); }
Напомню, что для многих элементов в классе CWndContainer (хранилище указателей на все элементы графического интерфейса) в структуре WindowElements объявлены персональные массивы. Чтобы получить количество элементов определенного типа из этого списка, реализован универсальный метод CWndContainer::ElementsTotal(). Передав в него индекс окна и тип элемента, мы получаем их количество в графическом интерфейсе MQL-приложения. Для указания типа элемента в файл Enums.mqh добавлено новое перечисление ENUM_ELEMENT_TYPE:
//+------------------------------------------------------------------+ //| Перечисление типов элементов | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_TYPE { E_CONTEXT_MENU =0, E_COMBO_BOX =1, E_SPLIT_BUTTON =2, E_MENU_BAR =3, E_MENU_ITEM =4, E_DROP_LIST =5, E_SCROLL =6, E_TABLE =7, E_TABS =8, E_SLIDER =9, E_CALENDAR =10, E_DROP_CALENDAR =11, E_SUB_CHART =12, E_PICTURES_SLIDER =13, E_TIME_EDIT =14, E_TEXT_BOX =15, E_TREE_VIEW =16, E_FILE_NAVIGATOR =17, E_TOOLTIP =18 };
Код метода CWndContainer::ElementsTotal() представлен в следующем листинге:
//+------------------------------------------------------------------+ //| Кол-во элементов по указанному индексу окна указанного типа | //+------------------------------------------------------------------+ int CWndContainer::ElementsTotal(const int window_index,const ENUM_ELEMENT_TYPE type) { //--- Проверка на выход из диапазона int index=CheckOutOfRange(window_index); if(index==WRONG_VALUE) return(WRONG_VALUE); //--- int elements_total=0; //--- switch(type) { case E_CONTEXT_MENU : elements_total=::ArraySize(m_wnd[index].m_context_menus); break; case E_COMBO_BOX : elements_total=::ArraySize(m_wnd[index].m_combo_boxes); break; case E_SPLIT_BUTTON : elements_total=::ArraySize(m_wnd[index].m_split_buttons); break; case E_MENU_BAR : elements_total=::ArraySize(m_wnd[index].m_menu_bars); break; case E_MENU_ITEM : elements_total=::ArraySize(m_wnd[index].m_menu_items); break; case E_DROP_LIST : elements_total=::ArraySize(m_wnd[index].m_drop_lists); break; case E_SCROLL : elements_total=::ArraySize(m_wnd[index].m_scrolls); break; case E_TABLE : elements_total=::ArraySize(m_wnd[index].m_tables); break; case E_TABS : elements_total=::ArraySize(m_wnd[index].m_tabs); break; case E_SLIDER : elements_total=::ArraySize(m_wnd[index].m_sliders); break; case E_CALENDAR : elements_total=::ArraySize(m_wnd[index].m_calendars); break; case E_DROP_CALENDAR : elements_total=::ArraySize(m_wnd[index].m_drop_calendars); break; case E_SUB_CHART : elements_total=::ArraySize(m_wnd[index].m_sub_charts); break; case E_PICTURES_SLIDER : elements_total=::ArraySize(m_wnd[index].m_pictures_slider); break; case E_TIME_EDIT : elements_total=::ArraySize(m_wnd[index].m_time_edits); break; case E_TEXT_BOX : elements_total=::ArraySize(m_wnd[index].m_text_boxes); break; case E_TREE_VIEW : elements_total=::ArraySize(m_wnd[index].m_treeview_lists); break; case E_FILE_NAVIGATOR : elements_total=::ArraySize(m_wnd[index].m_file_navigators); break; case E_TOOLTIP : elements_total=::ArraySize(m_wnd[index].m_tooltips); break; } //--- Вернуть количество элементов указанного типа return(elements_total); }
Чтобы снизить нагрузку на процессор, понадобилось добавить в структуру WindowElements ещё несколько массивов, в которых будут сохраняться указатели элементов следующих категорий.
- Массив главных элементов
- Массив элементов с таймером
- Массив видимых и доступных для обработки элементов
- Массив элементов, у которых включен режим автоматического изменения размеров по оси X
- Массив элементов, у которых включен режим автоматического изменения размеров по оси Y
class CWndContainer { protected: ... //--- Структура массивов элементов struct WindowElements { ... //--- Массив главных элементов CElement *m_main_elements[]; //--- Элементы с таймером CElement *m_timer_elements[]; //--- Видимые и доступные в данный момент элементы CElement *m_available_elements[]; //--- Элементы с автоизменением размеров по оси X CElement *m_auto_x_resize_elements[]; //--- Элементы с автоизменением размеров по оси Y CElement *m_auto_y_resize_elements[]; ... }; //--- Массив массивов элементов для каждого окна WindowElements m_wnd[]; ... };
Размеры этих массивов получаем соответствующими методами:
class CWndContainer { public: //--- Количество главных элементов int MainElementsTotal(const int window_index); //--- Количество элементов с таймерами int TimerElementsTotal(const int window_index); //--- Количество элементов с авто-ресайзом по оси X int AutoXResizeElementsTotal(const int window_index); //--- Количество элементов с авто-ресайзом по оси Y int AutoYResizeElementsTotal(const int window_index); //--- Количество доступных в данный момент элементов int AvailableElementsTotal(const int window_index); };
В массив, предназначенный для главных элементов, указатели добавляются в методе CWndContainer::AddToElementsArray(). Сокращённая версия этого метода:
//+------------------------------------------------------------------+ //| Добавляет указатель в массив элементов | //+------------------------------------------------------------------+ void CWndContainer::AddToElementsArray(const int window_index,CElementBase &object) { ... //--- Добавим в массив главных элементов last_index=ResizeArray(m_wnd[window_index].m_main_elements); m_wnd[window_index].m_main_elements[last_index]=::GetPointer(object); ... }
Массивы других категорий формируются в классе CWndEvents (см. ниже). Для добавления указателей в них используются отдельные методы.
class CWndContainer { protected: //--- Добавляет указатель в массив элементов с таймерами void AddTimerElement(const int window_index,CElement &object); //--- Добавляет указатель в массив элементов с авто-ресайзом по оси X void AddAutoXResizeElement(const int window_index,CElement &object); //--- Добавляет указатель в массив элементов с авто-ресайзом по оси Y void AddAutoYResizeElement(const int window_index,CElement &object); //--- Добавляет указатель в массив доступных на данный элементов void AddAvailableElement(const int window_index,CElement &object); }; //+------------------------------------------------------------------+ //| Добавляет указатель в массив элементов с таймерами | //+------------------------------------------------------------------+ void CWndContainer::AddTimerElement(const int window_index,CElement &object) { int last_index=ResizeArray(m_wnd[window_index].m_timer_elements); m_wnd[window_index].m_timer_elements[last_index]=::GetPointer(object); } //+------------------------------------------------------------------+ //| Добавляет указатель в массив элементов с авто-ресайзом (X) | //+------------------------------------------------------------------+ void CWndContainer::AddAutoXResizeElement(const int window_index,CElement &object) { int last_index=ResizeArray(m_wnd[window_index].m_auto_x_resize_elements); m_wnd[window_index].m_auto_x_resize_elements[last_index]=::GetPointer(object); } //+------------------------------------------------------------------+ //| Добавляет указатель в массив элементов с авто-ресайзом (Y) | //+------------------------------------------------------------------+ void CWndContainer::AddAutoYResizeElement(const int window_index,CElement &object) { int last_index=ResizeArray(m_wnd[window_index].m_auto_y_resize_elements); m_wnd[window_index].m_auto_y_resize_elements[last_index]=::GetPointer(object); } //+------------------------------------------------------------------+ //| Добавляет указатель в массив доступных элементов | //+------------------------------------------------------------------+ void CWndContainer::AddAvailableElement(const int window_index,CElement &object) { int last_index=ResizeArray(m_wnd[window_index].m_available_elements); m_wnd[window_index].m_available_elements[last_index]=::GetPointer(object); }
В классе CWndEvents тоже появились новые методы для внутреннего использования. Так, нам понадобился метод CWndEvents::Hide() для скрытия всех элементов графического интерфейса. Здесь используется двойной цикл: сначала скрываются формы, а уже во втором внутреннем цикле — элементы, привязанные к форме. Обратите внимание, что в этом методе второй цикл проходит по массиву элементов, состоящему из указателей на главные элементы. Методы элементов Hide() и Show() теперь устроены таким образом, что затрагивают всю цепочку этих методов у подключенных элементов на всю глубину вложенности.
//+------------------------------------------------------------------+ //| Класс для обработки событий | //+------------------------------------------------------------------+ class CWndEvents : public CWndContainer { protected: //--- Скрытие всех элементов void Hide(); }; //+------------------------------------------------------------------+ //| Скрытие элементов | //+------------------------------------------------------------------+ void CWndEvents::Hide(void) { int windows_total=CWndContainer::WindowsTotal(); for(int w=0; w<windows_total; w++) { m_windows[w].Hide(); int main_total=MainElementsTotal(w); for(int e=0; e<main_total; e++) { CElement *el=m_wnd[w].m_main_elements[e]; el.Hide(); } } }
Также в этом классе появился метод CWndEvents::Show() для показа элементов по указанной форме. Сначала показывается указанное в аргументе окно. Затем, если окно не в свёрнутом режиме, делаются видимыми все элементы привязанные к этой форме. В этом цикле пропускаются лишь те элементы, которые (1) являются выпадающими или, (2) если главным элементом у них обозначен элемент «Вкладки». Элементы на вкладках затем, уже вне цикла, показываются с помощью метода CWndEvents::ShowTabElements().
class CWndEvents : public CWndContainer { protected: //--- Показ элементов указанного окна void Show(const uint window_index); }; //+------------------------------------------------------------------+ //| Показ элементов указанного окна | //+------------------------------------------------------------------+ void CWndEvents::Show(const uint window_index) { //--- Показать элементы указанного окна m_windows[window_index].Show(); //--- Если окно не свёрнуто if(!m_windows[window_index].IsMinimized()) { int main_total=MainElementsTotal(window_index); for(int e=0; e<main_total; e++) { CElement *el=m_wnd[window_index].m_main_elements[e]; //--- Показать элемент, если он (1) не выпадающий и (2) его главный элемент не вкладки if(!el.IsDropdown() && dynamic_cast<CTabs*>(el.MainPointer())==NULL) el.Show(); } //--- Показать элементы только выделенных вкладок ShowTabElements(window_index); } }
Понадобится метод CWndEvents::Update() для перерисовки всех элементов графического интерфейса MQL-приложения. Этот метод может работать в двух режимах: (1) полностью перерисовать все элементы или (2) применить ранее внесённые изменения. Для полной перерисовки и обновления графического интерфейса нужно передать значение true.
class CWndEvents : public CWndContainer { protected: //--- Перерисовка элементов void Update(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Перерисовка элементов | //+------------------------------------------------------------------+ void CWndEvents::Update(const bool redraw=false) { int windows_total=CWndContainer::WindowsTotal(); for(int w=0; w<windows_total; w++) { //--- Перерисовка элементов int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) { CElement *el=m_wnd[w].m_elements[e]; el.Update(redraw); } } }
Об этих методах мы поговорим чуть ниже, а пока рассмотрим ряд методов, предназначенных для формирования массивов по рассмотренным категориям.
В предыдущей версии по таймеру плавно изменялся цвет элементов при наведении на них курсора мыши. Чтобы уменьшить объема и снизить потребление ресурсов, от этого излишества я отказался. Поэтому в текущей версии библиотеки таймер используется не во всех элементах. Им снабжена только ускоренная прокрутка (1) ползунков скроллинга, (2) значений в числовых полях ввода и (3) дат в календаре. Поэтому в методе CWndEvents::FormTimerElementsArray() в соответствующий массив добавляем только соответствующие элементы (см. листинг кода ниже).
Так как указатели на элементы хранятся в массивах базового типа элементов (CElement), то здесь и во многих других методах классов используется динамическое приведение типов (dynamic_cast) для определения производного типа элементов.
class CWndEvents : public CWndContainer { protected: //--- Формирует массив элементов с таймером void FormTimerElementsArray(void); }; //+------------------------------------------------------------------+ //| Формирует массив элементов с таймером | //+------------------------------------------------------------------+ void CWndEvents::FormTimerElementsArray(void) { int windows_total=CWndContainer::WindowsTotal(); for(int w=0; w<windows_total; w++) { int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) { CElement *el=m_wnd[w].m_elements[e]; //--- if(dynamic_cast<CCalendar *>(el)!=NULL || dynamic_cast<CColorPicker *>(el)!=NULL || dynamic_cast<CListView *>(el)!=NULL || dynamic_cast<CTable *>(el)!=NULL || dynamic_cast<CTextBox *>(el)!=NULL || dynamic_cast<CTextEdit *>(el)!=NULL || dynamic_cast<CTreeView *>(el)!=NULL) { CWndContainer::AddTimerElement(w,el); } } } }
Теперь таймер стал гораздо легче: он уже должен проверять не весь список элементов, как раньше, а только те, в которых заложена эта функция:
//+------------------------------------------------------------------+ //| Проверка событий всех элементов по таймеру | //+------------------------------------------------------------------+ void CWndEvents::CheckElementsEventsTimer(void) { int awi=m_active_window_index; int timer_elements_total=CWndContainer::TimerElementsTotal(awi); for(int e=0; e<timer_elements_total; e++) { CElement *el=m_wnd[awi].m_timer_elements[e]; if(el.IsVisible()) el.OnEventTimer(); } }
Обработка наведения курсора мыши тоже касается не всех элементов графического интерфейса. Из массива доступных элементов для такой обработки теперь исключены:
- CButtonsGroup — группа кнопок;
- CFileNavigator — файловый навигатор;
- CLineGraph — линейный график;
- CPicture — картинка;
- CPicturesSlider — слайдер картинок;
- CProgressBar — индикатор прогресса;
- CSeparateLine — разделительная линия;
- CStatusBar — статусная строка;
- CTabs — вкладки;
- CTextLabel — текстовая метка.
Все эти элементы не подсвечиваются при наведении курсора мыши. Правда, у некоторых из них есть подключенные элементы, которые подсвечиваются. Но поскольку при формировании массива доступных элементов в цикле используется общий массив, то подключенные элементы будут участвовать в отборе. В массив будут отобраны все элементы, которые видимы, доступны и не заблокированы.
class CWndEvents : public CWndContainer { protected: //--- Формирует массив доступных элементов void FormAvailableElementsArray(void); }; //+------------------------------------------------------------------+ //| Формирует массив доступных элементов | //+------------------------------------------------------------------+ void CWndEvents::FormAvailableElementsArray(void) { //--- Индекс окна int awi=m_active_window_index; //--- Общее количество элементов int elements_total=CWndContainer::ElementsTotal(awi); //--- Очистим массив ::ArrayFree(m_wnd[awi].m_available_elements); //--- for(int e=0; e<elements_total; e++) { CElement *el=m_wnd[awi].m_elements[e]; //--- Добавляем только видимые и доступные для обработки элементы if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked()) continue; //--- Исключить элементы не требующие обработки по наведению курсора мыши if(dynamic_cast<CButtonsGroup *>(el)==NULL && dynamic_cast<CFileNavigator *>(el)==NULL && dynamic_cast<CLineGraph *>(el)==NULL && dynamic_cast<CPicture *>(el)==NULL && dynamic_cast<CPicturesSlider *>(el)==NULL && dynamic_cast<CProgressBar *>(el)==NULL && dynamic_cast<CSeparateLine *>(el)==NULL && dynamic_cast<CStatusBar *>(el)==NULL && dynamic_cast<CTabs *>(el)==NULL && dynamic_cast<CTextLabel *>(el)==NULL) { AddAvailableElement(awi,el); } } }
Осталось рассмотреть методы CWndEvents::FormAutoXResizeElementsArray() и CWndEvents::FormAutoYResizeElementsArray(), формирующие массивы с указателями на элементы, у которых включены режимы автоизменения размеров. Такие элементы ориентируются на размеры главных элементов, к которым они привязаны. Не у всех элементов определён код методов для автоизменения размеров. Перечислим те, у которых он есть:
Элементы, у которых определён код в виртуальном методе CElement::ChangeWidthByRightWindowSide() для автоизменения ширины:
- CButton — кнопка.
- CFileNavigator — файловый навигатор.
- CLineGraph — линейный график.
- CListView — список.
- CMenuBar — главное меню.
- CProgressBar — индикатор выполнения.
- CStandardChart — стандартный график.
- CStatusBar — статусная строка.
- CTable — таблица.
- CTabs — вкладки.
- CTextBox — текстовое поле ввода.
- CTextEdit — поле ввода.
- CTreeView — древовидный список.
Элементы, у которых определён код в виртуальном методе CElement:: ChangeHeightByBottomWindowSide() для автоизменения высоты:
- CLineGraph — линейный график.
- CListView — список.
- CStandardChart — стандартный график.
- CTable — таблица.
- CTabs — вкладки.
- CTextBox — текстовое поле ввода.
При создании массивов для этих категорий проверяется, включены ли в этих элементах режимы автоизменения размеров, и если включены, то они добавляются в массив. Не будем приводить код этих методов: похожие на них рассмотрены выше.
Теперь разберёмся, в какие моменты формируются массивы по перечисленным выше категориям. В главном методе создания графического интерфейса, который пользователь библиотеки формирует самостоятельно, после успешного создания всех указанных элементов, для их отображения на графике теперь нужно вызвать только один метод CWndEvents::CompletedGUI(). Он сигнализирует программе, что создание графического интерфейса MQL-приложения завершено.
Рассмотрим подробнее метод CWndEvents::CompletedGUI(). В нём вызываются все методы, описанные выше в этом разделе. Сначала все элементы графического интерфейса скрываются. Ни один из них ещё не нарисован, поэтому, чтобы исключить постепенное поочерёдное появление, их нужно сначала скрыть перед началом отрисовки. Далее осуществляется сама отрисовка, и к каждому элементу применяются последние изменения. После этого нужно отобразить элементы только главного окна. Затем формируются массивы указателей на элементы по категориям, после чего в самом конце метода график обновляется.
class CWndEvents : public CWndContainer { protected: //--- Завершение создания GUI void CompletedGUI(void); }; //+------------------------------------------------------------------+ //| Завершение создания GUI | //+------------------------------------------------------------------+ void CWndEvents::CompletedGUI(void) { //--- Выйти, если ещё нет ни одного окна int windows_total=CWndContainer::WindowsTotal(); if(windows_total<1) return; //--- Показать комментарий информирующий пользователя ::Comment("Update. Please wait..."); //--- Скрыть элементы Hide(); //--- Нарисовать элементы Update(true); //--- Показать элементы активированного окна Show(m_active_window_index); //--- Сформируем массив элементов с таймером FormTimerElementsArray(); //--- Сформируем массив видимых и при этом доступных элементов FormAvailableElementsArray(); //--- Сформируем массивы элементов с авторесайзом FormAutoXResizeElementsArray(); FormAutoYResizeElementsArray(); //--- Перерисовать график m_chart.Redraw(); //--- Очистить комментарий ::Comment(""); }
Метод для проверки и обработки событий элементов CWndEvents::CheckElementsEvents() существенно изменился. Остановимся на этом подробнее.
Теперь в этом методе два блока обработки событий. Один блок предназначен исключительно для обработки перемещения курсора мыши (CHARTEVENT_MOUSE_MOVE). Вместо того, чтобы проходить в цикле по списку всех элементов активированного окна, как это было раньше, теперь цикл идет только по доступным для обработки элементов. Именно для этого и формировался массив с указателями на доступные элементы. В графическом интерфейсе большого MQL-приложения может быть несколько сотен и даже тысяч различных элементов управления, а в текущий момент времени могут быть видимы и доступны только несколько из всего этого списка. Такой подход очень сильно разгружает ресурсы процессора.
Ещё одно изменение заключается в том, что теперь проверки (1) подокна, в котором находится форма и (2) фокуса над элементом, осуществляются во внешнем цикле, а не в обработчиках каждого класса элементов. Таким образом, теперь проверки, относящиеся к каждому элементу, находятся в одном месте. Это будет удобно в будущем, если нужно будет внести изменения в алгоритм обработки событий.
Все остальные типы событий обрабатываются в отдельном блоке. В текущей версии проход осуществляется по всему списку элементов графического интерфейса. Все проверки, которые раньше были в классах элементов, теперь также вынесены во внешний цикл.
В самом конце метода событие направляется в пользовательский класс MQL-приложения.
//+------------------------------------------------------------------+ //| Проверка событий элементов управления | //+------------------------------------------------------------------+ void CWndEvents::CheckElementsEvents(void) { //--- Обработка события перемещения курсора мыши if(m_id==CHARTEVENT_MOUSE_MOVE) { //--- Выйти, если форма находится в другом подокне графика if(!m_windows[m_active_window_index].CheckSubwindowNumber()) return; //--- Проверяем только доступные элементы int available_elements_total=CWndContainer::AvailableElementsTotal(m_active_window_index); for(int e=0; e<available_elements_total; e++) { CElement *el=m_wnd[m_active_window_index].m_available_elements[e]; //--- Проверка фокуса над элементами el.CheckMouseFocus(); //--- Обработка события el.OnEvent(m_id,m_lparam,m_dparam,m_sparam); } } //--- Все события, кроме перемещения курсора мыши else { int elements_total=CWndContainer::ElementsTotal(m_active_window_index); for(int e=0; e<elements_total; e++) { //--- Проверяем только доступные элементы CElement *el=m_wnd[m_active_window_index].m_elements[e]; if(!el.IsVisible() || !el.IsAvailable() || el.IsLocked()) continue; //--- Обработка события в обработчике элемента el.OnEvent(m_id,m_lparam,m_dparam,m_sparam); } } //--- Направление события в файл приложения OnEvent(m_id,m_lparam,m_dparam,m_sparam); }
Метод CWndEvents::FormAvailableElementsArray() для формирования массива видимых и при этом доступных для обработки элементов вызывается в следующих случаях:
- Открытие диалогового окна. После открытия диалогового окна генерируется событие ON_OPEN_DIALOG_BOX, которое обрабатывается в методе CWndEvents::OnOpenDialogBox(). После обработки этого события нужно сформировать массив доступных элементов открытого окна.
- Изменение в графическом интерфейсе. Любое изменение в графическом интерфейсе при взаимодействии с ним генерирует событие ON_CHANGE_GUI. Его обрабатывает новый приватный (private) метод CWndEvents::OnChangeGUI(). Здесь по приходу события ON_CHANGE_GUI сначала формируется массив доступных элементов. Затем все всплывающие подсказки перемещаются на верхний слой. В конце метода график перерисовывается для отображения последних изменений.
class CWndEvents : public CWndContainer { private: //--- Изменения в графическом интерфейсе bool OnChangeGUI(void); }; //+------------------------------------------------------------------+ //| Событие изменений в графическом интерфейсе | //+------------------------------------------------------------------+ bool CWndEvents::OnChangeGUI(void) { //--- Если сигнал об изменении в графическом интерфейсе if(m_id!=CHARTEVENT_CUSTOM+ON_CHANGE_GUI) return(false); //--- Сформируем массив видимых и при этом доступных элементов FormAvailableElementsArray(); //--- Переместить всплывающие подсказки на верхний слой ResetTooltips(); //--- Перерисовать график m_chart.Redraw(); return(true); }
Далее рассмотрим, как обрабатывается событие с идентификатором ON_SET_AVAILABLE для определения элементов, которые будут доступны для обработки.
Для обработки события ON_SET_AVAILABLE реализован метод CWndEvents::OnSetAvailable(). Но прежде чем перейти к описанию его кода, нужно рассмотреть ряд вспомогательных методов. Есть 10 элементов графического интерфейса, которые генерируют событие с таким идентификатором. У них всех есть средства для определения их активированного состояния. Перечислим их:
- Главное меню — CMenuBar::State().
- Пункт меню — CMenuItem::GetContextMenuPointer().IsVisible().
- Сдвоенная кнопка — CSplitButton::GetContextMenuPointer().IsVisible().
- Комбобокс — CComboBox::GetListViewPointer().IsVisible().
- Выпадающий календарь — DropCalendar::GetCalendarPointer().IsVisible().
- Полоса прокрутки — CScroll::State().
- Таблица — CTable::ColumnResizeControl().
- Числовой слайдер — CSlider::State().
- Древовидный список — CTreeView::GetMousePointer().State().
- Стандартный график — CStandartChart::GetMousePointer().IsVisible().
У каждого из этих элементов есть персональные массивы в классе CWndContainer. В классе CWndEvents реализованы методы для определения, какой из этих элементов активирован в текущий момент. Все эти методы возвращают индекс активированного элемента в своём персональном массиве.
class CWndEvents : public CWndContainer { private: //--- Возвращает индекс активированного главного меню int ActivatedMenuBarIndex(void); //--- Возвращает индекс активированного пункта меню int ActivatedMenuItemIndex(void); //--- Возвращает индекс активированной сдвоенной кнопки int ActivatedSplitButtonIndex(void); //--- Возвращает индекс активированного комбо-бокса int ActivatedComboBoxIndex(void); //--- Возвращает индекс активированного выпадающего календаря int ActivatedDropCalendarIndex(void); //--- Возвращает индекс активированной полосы прокрутки int ActivatedScrollIndex(void); //--- Возвращает индекс активированной таблицы int ActivatedTableIndex(void); //--- Возвращает индекс активированного слайдера int ActivatedSliderIndex(void); //--- Возвращает индекс активированного древовидного списка int ActivatedTreeViewIndex(void); //--- Возвращает индекс активированного стандартного графика int ActivatedSubChartIndex(void); };
Так как практически все эти методы отличаются только условиями для определения состояния элементов, то приведём здесь для примера код только одного из них. В листинге ниже представлен код метода CWndEvents::ActivatedTreeViewIndex(), который возвращает индекс активированного древовидного списка. Если у этого типа элемента включен режим пунктов-вкладок, то проверка отклоняется.
//+------------------------------------------------------------------+ //| Возвращает индекс активированного древовидного списка | //+------------------------------------------------------------------+ int CWndEvents::ActivatedTreeViewIndex(void) { int index=WRONG_VALUE; //--- int total=ElementsTotal(m_active_window_index,E_TREE_VIEW); for(int i=0; i<total; i++) { CTreeView *el=m_wnd[m_active_window_index].m_treeview_lists[i]; //--- Перейти к следующему, если включен режим вкладок if(el.TabItemsMode()) continue; //--- Если процесс изменения ширины списков if(el.GetMousePointer().State()) { index=i; break; } } return(index); }
Для установки состояний доступности элементов предназначен метод CWndEvents::SetAvailable(). В качестве аргументов нужно передать (1) индекс формы, с которой работаем и (2) состояние, в которое нужно установить элементы.
Если нужно сделать все элементы недоступными, то просто проходим по всем ним в цикле и устанавливаем значение false.
Если нужно сделать элементы доступными, то для древовидных списков вызывается перегруженный одноимённый метод CTreeView::IsAvailable(), в котором есть два режима для установки состояния: (1) только для главного элемента и (2) для всех элементов на всю глубину. Поэтому здесь используется динамическое приведение типов, чтобы получить указатель на элемент производного класса элемента.
class CWndEvents : public CWndContainer { protected: //--- Устанавливает состояние доступности элементов void SetAvailable(const uint window_index,const bool state); }; //+------------------------------------------------------------------+ //| Устанавливает состояние доступности элементов | //+------------------------------------------------------------------+ void CWndEvents::SetAvailable(const uint window_index,const bool state) { //--- Получим количество главных элементов int main_total=MainElementsTotal(window_index); //--- Если нужно сделать элементы недоступными if(!state) { m_windows[window_index].IsAvailable(state); for(int e=0; e<main_total; e++) { CElement *el=m_wnd[window_index].m_main_elements[e]; el.IsAvailable(state); } } else { m_windows[window_index].IsAvailable(state); for(int e=0; e<main_total; e++) { CElement *el=m_wnd[window_index].m_main_elements[e]; //--- Если это древовидный список if(dynamic_cast<CTreeView*>(el)!=NULL) { CTreeView *tv=dynamic_cast<CTreeView*>(el); tv.IsAvailable(true); continue; } //--- Если это файловый навигатор if(dynamic_cast<CFileNavigator*>(el)!=NULL) { CFileNavigator *fn =dynamic_cast<CFileNavigator*>(el); CTreeView *tv =fn.GetTreeViewPointer(); fn.IsAvailable(state); tv.IsAvailable(state); continue; } //--- Сделать элемент доступным el.IsAvailable(state); } } }
Для пунктов меню, за которыми закреплены контекстные меню, понадобился метод, с помощью которого можно пройтись на всю глубину открытых контекстных меню, получив к ним доступ. В данном случае нужно будет делать контекстные меню доступными для обработки. Реализуем это рекурсивным способом.
Ниже представлен код метода CWndEvents::CheckContextMenu(). Вначале передаем в него объект пункта меню и пробуем получить указатель на контекстное меню. Если указатель корректный, то проверяем, открыто ли это контекстное меню. Если да, то устанавливаем ему признак доступного элемента. Далее в цикле устанавливаем статус доступности всем пунктам этого меню. При этом проверяем каждый пункт на наличие в нем контекстного меню методом CWndEvents::CheckContextMenu().
class CWndEvents : public CWndContainer { private: //--- Проверяет и делает контекстное меню доступным void CheckContextMenu(CMenuItem &object); }; //+------------------------------------------------------------------+ //| Рекурсивно проверяет и делает контекстные меню доступными | //+------------------------------------------------------------------+ void CWndEvents::CheckContextMenu(CMenuItem &object) { //--- Получим указатель контекстного меню CContextMenu *cm=object.GetContextMenuPointer(); //--- Выйти, если контекстного меню нет в пункте if(::CheckPointer(cm)==POINTER_INVALID) return; //--- Выйти, если контекстное меню есть, но скрыто if(!cm.IsVisible()) return; //--- Установить признаки доступного элемента cm.IsAvailable(true); //--- int items_total=cm.ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Установить признаки доступного элемента CMenuItem *mi=cm.GetItemPointer(i); mi.IsAvailable(true); //--- Проверим, есть ли в этом пункте контекстное меню CheckContextMenu(mi); } }
Теперь рассмотрим метод CWndEvents::OnSetAvailable(), в котором обрабатывается событие для определения доступных элементов управления.
Если пришло пользовательское событие с идентификатором ON_SET_AVAILABLE, сначала нужно определить, есть ли в данный момент активированные элементы. В локальных переменных сохраняются индексы активированных элементов для быстрого доступа к их персональным массивам.
Если пришёл сигнал на определение доступных элементов, то сначала отключаем доступ во всём списке. Если же сигнал на восстановление, то после проверки отсутствия наличия активированных элементов, восстанавливается доступ во всём списке и программа выходит из метода.
Если программа дошла до следующего блока кода в этом методе, то это означает, что это или (1) сигнал на определение доступных элементов, или (2) на восстановление, но при этом есть активированный выпадающий календарь. Вторая ситуация может возникнуть, когда был открыт выпадающий календарь с активированным комбобоксом, в котором был закрыт выпадающий список.
Если одно из описанных условий выполнено, пробуем получить указатель на активированный элемент. Если указатель не получен, то программа выйдет из метода.
Если указатель получен, то элемент делается доступным. Для некоторых элементов есть нюансы, каким образом это будет сделано. Перечислим все эти случаи:
- Главное меню (CMenuBar). Если оно активировано, то нужно сделать доступными и все открытые контекстные меню, относящиеся к нему. Для этого в листинге кода выше рассматривался рекурсивный метод CWndEvents::CheckContextMenu().
- Пункт меню (CMenuItem). Пункты меню могут быть независимыми элементами, к которым можно присоединять контекстные меню. Поэтому, если активирован элемент такого типа, то здесь также делается доступным сам элемент (пункт меню), а также все контекстные меню, которые из него открыты.
- Полоса прокрутки (CScroll). Если активирована полоса прокрутки (ползунок в процессе перемещения), то сделать доступными нужно все элементы, начиная от главного. Например, если полоса прокрутки присоединена к списку, то доступным станет список и все элементы, которые к нему подключены на всю глубину.
- Древовидный список (CTreeView). Этот элемент может быть активирован, когда изменяется ширина его списков. Нужно исключить обработку пунктов списков по наведению курсора мыши, а сам древовидный список — сделать доступным для обработки.
Ниже можно подробнее ознакомиться с кодом метода CWndEvents::OnSetAvailable():
class CWndEvents : public CWndContainer { private: //--- Определение доступных элементов bool OnSetAvailable(void); }; //+------------------------------------------------------------------+ //| Событие для определения доступных элементов управления | //+------------------------------------------------------------------+ bool CWndEvents::OnSetAvailable(void) { //--- Если сигнал об изменении доступности элементов if(m_id!=CHARTEVENT_CUSTOM+ON_SET_AVAILABLE) return(false); //--- Сигнал на установку/восстановление bool is_restore=(bool)m_dparam; //--- Определим активные элементы int mb_index =ActivatedMenuBarIndex(); int mi_index =ActivatedMenuItemIndex(); int sb_index =ActivatedSplitButtonIndex(); int cb_index =ActivatedComboBoxIndex(); int dc_index =ActivatedDropCalendarIndex(); int sc_index =ActivatedScrollIndex(); int tl_index =ActivatedTableIndex(); int sd_index =ActivatedSliderIndex(); int tv_index =ActivatedTreeViewIndex(); int ch_index =ActivatedSubChartIndex(); //--- Если сигнал на определение доступных элементов, сначала отключаем доступ if(!is_restore) SetAvailable(m_active_window_index,false); //--- Восстанавливаем, только если нет активированных элементов else { if(mb_index==WRONG_VALUE && mi_index==WRONG_VALUE && sb_index==WRONG_VALUE && dc_index==WRONG_VALUE && cb_index==WRONG_VALUE && sc_index==WRONG_VALUE && tl_index==WRONG_VALUE && sd_index==WRONG_VALUE && tv_index==WRONG_VALUE && ch_index==WRONG_VALUE) { SetAvailable(m_active_window_index,true); return(true); } } //--- Если (1) сигнал на отключение доступа или (2) на восстановление для выпадающего календаря if(!is_restore || (is_restore && dc_index!=WRONG_VALUE)) { CElement *el=NULL; //--- Главное меню if(mb_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_menu_bars[mb_index]; } //--- Пункт меню else if(mi_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_menu_items[mi_index]; } //--- Сдвоенная кнопка else if(sb_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_split_buttons[sb_index]; } //--- Выпадающий календарь без выпадающего списка else if(dc_index!=WRONG_VALUE && cb_index==WRONG_VALUE) { el=m_wnd[m_active_window_index].m_drop_calendars[dc_index]; } //--- Выпадающий список else if(cb_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_combo_boxes[cb_index]; } //--- Полоса прокрутки else if(sc_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_scrolls[sc_index]; } //--- Таблица else if(tl_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_tables[tl_index]; } //--- Слайдер else if(sd_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_sliders[sd_index]; } //--- Древовидный список else if(tv_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_treeview_lists[tv_index]; } //--- Стандартный график else if(ch_index!=WRONG_VALUE) { el=m_wnd[m_active_window_index].m_sub_charts[ch_index]; } //--- Выйти, если указатель на элемент не получен if(::CheckPointer(el)==POINTER_INVALID) return(true); //--- Блок для главного меню if(mb_index!=WRONG_VALUE) { //--- Сделать доступными главное меню и его видимые контекстные меню el.IsAvailable(true); //--- CMenuBar *mb=dynamic_cast<CMenuBar*>(el); int items_total=mb.ItemsTotal(); for(int i=0; i<items_total; i++) { CMenuItem *mi=mb.GetItemPointer(i); mi.IsAvailable(true); //--- Проверяет и делает контекстное меню доступным CheckContextMenu(mi); } } //--- Блок для пункта меню if(mi_index!=WRONG_VALUE) { CMenuItem *mi=dynamic_cast<CMenuItem*>(el); mi.IsAvailable(true); //--- Проверяет и делает контекстное меню доступным CheckContextMenu(mi); } //--- Блок для полос прокрутки else if(sc_index!=WRONG_VALUE) { //--- Сделать доступным начиная от главного узла el.MainPointer().IsAvailable(true); } //--- Блок для древовидного списка else if(tv_index!=WRONG_VALUE) { //--- Заблокировать все элементы, кроме главного CTreeView *tv=dynamic_cast<CTreeView*>(el); tv.IsAvailable(true,true); int total=tv.ElementsTotal(); for(int i=0; i<total; i++) tv.Element(i).IsAvailable(false); } else { //--- Сделать элемент доступным el.IsAvailable(true); } } //--- return(true); }
Приложение для теста элементов
Для тестов реализовано MQL-приложение, в графическом интерфейсе которого есть все элементы библиотеки. Выглядит это так:
Рис. 12. Графический интерфейс тестового MQL-приложения.
В конце статьи можно скачать его для более подробного изучения.
Заключение
Эта версия библиотеки сильно отличается от той, которая была представлена в статье Графические интерфейсы X: Выделение текста в многострочном поле ввода (build 13). Проведена большая работа, которая коснулась практически всех файлов библиотеки. Теперь все элементы библиотеки рисуются на отдельных объектах. Читаемость кода повысилась, объём кода уменьшился приблизительно на 30%, а возможности расширились. Исправлено множество других ошибок и недоработок, о которых сообщали пользователи в комментариях к статье.
Если Вы уже начали создавать свои MQL-приложения, используя предыдущую версию библиотеки, то рекомендуется сначала скачать новую версию в отдельно установленную копию торгового терминала MetaTrader 5, чтобы в ней разобраться и всё тщательно протестировать.
На текущем этапе разработки библиотеки для создания графических интерфейсов её общая схема выглядит так, как показано на рисунке ниже. Это не окончательная версия библиотеки: в дальнейшем она будет развиваться и совершенствоваться.
Рис. 13. Структура библиотеки на текущей стадии разработки.
При возникновении вопросов по использованию материала, предоставленного в этих файлах, вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей этой серии или задать вопрос в комментариях к статье.
![Кроссплатформенный торговый советник: Мани-менеджмент](https://c.mql5.com/2/28/Cross_Platform_Expert_Advisor__1.png)
![Использование облачных хранилищ для обмена данными между терминалами](https://c.mql5.com/2/28/7l8-fbt8.png)
![Тестирование паттернов, возникающих при торговле корзинами валютных пар. Часть I](https://c.mql5.com/2/28/articles_234__1.png)
![Графические интерфейсы XI: Рефакторинг кода библиотеки (build 14.1)](https://c.mql5.com/2/28/MQL5-avatar-XI-build14.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Толь, в какой из статей этого цикла можно почитать о CKeys ?
Иногда при вводе ЧИСЕЛ в поле ввода, вводятся символы этих числовых клавиш, которые на клавише+Shift. Т.е., ввожу 2, а вводится @ ...
В некоторых ситуациях - не понял в каких (наверное зависит от количества и типа управляющих элементов на окне-форме), нажатие клавиши Backspace приводит к удалению управляющих элементов с окна-формы в обратном порядке их добавления. Не пойму почему он видит нажатие Backspace не как управляющий символ для поля ввода когда курсор в нём, а как команду на удаление управляющих элементов, находящихся на окне-форме. Т.е,: запускаю программу и начинаю нажимать Backspace- все управляющие элементы с каждым нажатием Backspace удаляются, начиная с последнего добавленного. В конечном итоге удаляется и уже ставшее пустым окно-форма.
ЗЫ. Возможно ли отключить реакцию на Backspace пока курсор не находится в поле ввода?
...
Кстати - а возможно ли фон этих кнопок сделать прозрачным? А то заголовок окна, например, серый, а кнопки синие...
Воспользоваться методами для получения указателей составных элементов. Это же правило распространяется на все элементы.
Толь, в какой из статей этого цикла можно почитать о CKeys ?
...
Т.е,: запускаю программу и начинаю нажимать Backspace- все управляющие элементы с каждым нажатием Backspace удаляются, начиная с последнего добавленного. В конечном итоге удаляется и уже ставшее пустым окно-форма.
...
Графические интерфейсы X: Элемент "Многострочное текстовое поле ввода" (build 8)
Не смог воспроизвести ситуацию, когда при нажатии клавиши Backspace удаляются элементы графического интерфейса.
Если в TestLibrary14 в Program.mqh вписать одну строчку - изменение цвета заголовка окна:
То сворачивание окна возвращает цвет к значению по умолчанию. А кнопки подсказок, сворачивания/разворачивания и закрытия окна перекрашиваются в цвет заголовка окна, который был программно установлен. А затем цвет этих кнопок изменяется на цвет по-умолчанию только при наведении курсора.
...
Забыл добавить изменение цвета заголовка формы по наведению курсора мыши. Будет в одном из следующих обновлений.
Сейчас цвет нужно установить для обоих состояний:
//---
Для кнопок цвет можно установить через их указатели и после создания формы.
//---
По моему мнению, термин "ядро библиотеки", который употребляется в статье, не совсем корректен. Ранее употреблялся термин "движок" библиотеки, но и он не совсем подходит.
Под понятием "ядро" должен подразумеваться статический массив, объединяющий в себе всю необходимую информацию, а под понятием "движок" - механизм работающий с этой информацией и реализующий комплекс задач.
Под понятием "библиотека" в программировании подразумевается разнообразный, разрозненный и самостоятельный функционал, служащий инструментарием для решения комплекса задач, и не объедененный вокруг единого центра. Данный инструментарий не является ни "движком", ни "ядром", а только набором средств для построения пользовательских механизмов.
Если библиотека содержит ядро и движок, то это уже не совсем библиотека, а система работающая как единое целое.
Превращение графической библиотеки в цельный механизм по созданию GUI, - это закономерный этап развития, после которого, библиотеки как таковой уже не будет. То есть, исчезнет необходимость в разрозненном функционале, поскольку готовый механизм решает комплекс задач гораздо эффективней.
Возможно сейчас библиотека превращается в такой механизм, но на данный момент этого еще не произошло. Поэтому, термины "ядро" и "движок" библиотеки не совсем корректны.
имхо.