Графические интерфейсы III: Группы простых и многофункциональных кнопок (Глава 2)
Содержание
- Введение
- Разработка класса для создания групп простых кнопок
- Разработка класса для создания групп радио-кнопок
- Разработка класса для создания групп кнопок с картинками
- Заключение
Введение
О том, для чего предназначена эта библиотека, более подробно можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце статей каждой части представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.
Первая глава третьей части серии рассказывала о простых и многофункциональных кнопках. Во второй главе мы рассмотрим группы взаимосвязанных кнопок. С их помощью в приложениях создаются элементы, позволяющие пользователю выбирать какую-то одну опцию из представленного набора (группы).
Разработка класса для создания групп простых кнопок
Группа простых кнопок представляет собой массив графических объектов типа OBJ_BUTTON. Отличительная особенность подобных элементов управления заключается в том, что одновременно может быть нажата только одна кнопка в группе. На текущий момент создать класс этого элемента можно двумя способами:
- создать группу из уже реализованного элемента типа CSimpleButton;
- создать группу из объектов примитивов типа CButton.
Второй вариант проще тем, что не нужно будет создавать дополнительный метод для того, чтобы каждый элемент типа CSimpleButton попал в базу указателей, поэтому воспользуемся им.
Создаём файл ButtonsGroup.mqh с классом CButtonsGroup в папке всех элементов (Controls) и сразу подключаем его к файлу WndContainer.mqh. В классе должны быть стандартные виртуальные методы и указатель на форму, как это было уже неоднократно показано во всех других ранее разработанных элементах. Поэтому не будем снова показывать их здесь, а сразу перейдём к описанию свойств и методов для их установки.
Какие-то свойства будут общими для каждой кнопки внутри одной группы, но будут и уникальные. Перечислим эти две группы свойств, относящиеся к внешнему виду кнопок.
Общие свойства кнопок:
- высота;
- цвет заблокированных кнопок;
- цвет рамок в доступном и заблокированном режимах;
- цвета текста в разных состояниях;
- приоритет на нажатие левой кнопкой мыши.
Уникальные свойства кнопок:
- состояние кнопки (нажата/отжата);
- отступы от крайней точки формы;
- текст;
- ширина;
- цвета кнопки в разных состояниях;
- градиенты для кнопок.
Отступы для каждой кнопки позволят организовать расположение в любой последовательности:
Рис. 1. Примеры расположения кнопок в группе.
Для объектов типа CButton и уникальных свойств кнопок объявим динамические массивы:
//+------------------------------------------------------------------+ //| Класс для создания группы простых кнопок | //+------------------------------------------------------------------+ class CButtonsGroup : public CElement { private: //--- Объекты для создания кнопки CButton m_buttons[]; //--- Градиенты кнопок struct ButtonsGradients { color m_buttons_color_array[]; }; ButtonsGradients m_buttons_total[]; //--- Свойства кнопок: // Массивы для уникальных свойств кнопок bool m_buttons_state[]; int m_buttons_x_gap[]; int m_buttons_y_gap[]; string m_buttons_text[]; int m_buttons_width[]; color m_buttons_color[]; color m_buttons_color_hover[]; color m_buttons_color_pressed[]; //--- Высота кнопок int m_button_y_size; //--- Цвет заблокированных кнопок color m_back_color_off; //--- Цвет рамки в активном и заблокированных режимах color m_border_color; color m_border_color_off; //--- Цвета текста color m_text_color; color m_text_color_off; color m_text_color_pressed; //--- Приоритет на нажатие левой кнопкой мыши int m_buttons_zorder; //--- public: //--- Количество кнопок int ButtonsTotal(void) const { return(::ArraySize(m_buttons)); } //--- (1) Высота кнопок void ButtonYSize(const int y_size) { m_button_y_size=y_size; } //--- (1) Цвета фона заблокированной кнопки и рамки ((2) доступен/(3) заблокирован) void BackColorOff(const color clr) { m_back_color_off=clr; } void BorderColor(const color clr) { m_border_color=clr; } void BorderColorOff(const color clr) { m_border_color_off=clr; } //--- Цвета текста void TextColor(const color clr) { m_text_color=clr; } void TextColorOff(const color clr) { m_text_color_off=clr; } void TextColorPressed(const color clr) { m_text_color_pressed=clr; } };
Размер динамических массивов будет определяться при формировании группы кнопок до её создания (установки на график). При каждом вызове метода CButtonsGroup::AddButton() массивы увеличиваются на один элемент, который заполняется переданными параметрами.
class CButtonsGroup : public CElement { public: //--- Добавляет кнопку с указанными свойствами до создания void AddButton(const int x_gap,const int y_gap,const string text,const int width, const color button_color,const color button_color_hover,const color button_color_pressed); }; //+------------------------------------------------------------------+ //| Добавляет кнопку | //+------------------------------------------------------------------+ void CButtonsGroup::AddButton(const int x_gap,const int y_gap,const string text,const int width, const color button_color,const color button_color_hover,const color pressed_button_color) { //--- Увеличим размер массивов на один элемент int array_size=::ArraySize(m_buttons); ::ArrayResize(m_buttons,array_size+1); ::ArrayResize(m_buttons_total,array_size+1); ::ArrayResize(m_buttons_state,array_size+1); ::ArrayResize(m_buttons_x_gap,array_size+1); ::ArrayResize(m_buttons_y_gap,array_size+1); ::ArrayResize(m_buttons_text,array_size+1); ::ArrayResize(m_buttons_width,array_size+1); ::ArrayResize(m_buttons_color,array_size+1); ::ArrayResize(m_buttons_color_hover,array_size+1); ::ArrayResize(m_buttons_color_pressed,array_size+1); //--- Сохраним значения переданных параметров m_buttons_x_gap[array_size] =x_gap; m_buttons_y_gap[array_size] =y_gap; m_buttons_text[array_size] =text; m_buttons_width[array_size] =width; m_buttons_color[array_size] =button_color; m_buttons_color_hover[array_size] =button_color_hover; m_buttons_color_pressed[array_size] =pressed_button_color; m_buttons_state[array_size] =false; }
В процессе создания группы кнопок, если до этого ни одной кнопки не было добавлено с помощью метода CButtonsGroup::AddButton(), процесс создания графического интерфейса будет остановлен, и в журнал выведется сообщение с подсказкой. Создание кнопок осуществляется в цикле (см. сокращённую версию метода CButtonsGroup::CreateButtons() в листинге кода ниже).
class CButtonsGroup : public CElement { public: //--- Методы для создания кнопки bool CreateButtonsGroup(const long chart_id,const int subwin,const int x,const int y); //--- private: bool CreateButtons(void); }; //+------------------------------------------------------------------+ //| Создаёт кнопки | //+------------------------------------------------------------------+ bool CButtonsGroup::CreateButtons(void) { //--- Координаты int l_x =m_x; int l_y =m_y; //--- Получим количество кнопок int buttons_total=ButtonsTotal(); //--- Если нет ни одной кнопки в группе, сообщить об этом if(buttons_total<1) { ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, " "когда в группе есть хотя бы одна кнопка! Воспользуйтесь методом CButtonsGroup::AddButton()"); return(false); } //--- Создадим указанное количество кнопок for(int i=0; i<buttons_total; i++) { //--- Формирование имени объекта //--- Расчёт координат //--- Установим кнопку //--- Установка свойств //--- Сохраним отступы от крайней точки панели, координаты и размеры //--- Инициализация массива градиента //--- Сохраним указатель объекта } //--- return(true); }
Сделаем два режима для этого типа группы кнопок. Для установки режима добавим в класс специальное поле и метод (см. листинг ниже). По умолчанию будет установлено значение false, и это будет означать, что включен режим, когда в группе могут быть отжаты все кнопки. Если установлено значение true, то в группе всегда будет нажата хотя бы одна кнопка.
class CButtonsGroup : public CElement { public: //--- Режим радио-кнопок bool m_radio_buttons_mode; //--- public: //--- Установка режима радио-кнопок void RadioButtonsMode(const bool flag) { m_radio_buttons_mode=flag; } };
Как в любом другом элементе управления, в этом классе должен быть метод для блокировки элемента. Для этого типа группы кнопок нужно проконтролировать, чтобы при разблокировке нажатая до этого кнопка восстанавливала внешний вид своего состояния.
class CButtonsGroup : public CElement { private: //--- Доступен/заблокирован bool m_buttons_group_state; //--- public: //--- Общее состояние группы кнопок (доступен/заблокирован) bool ButtonsGroupState(void) const { return(m_buttons_group_state); } void ButtonsGroupState(const bool state); }; //+------------------------------------------------------------------+ //| Изменение состояния кнопок | //+------------------------------------------------------------------+ void CButtonsGroup::ButtonsGroupState(const bool state) { m_buttons_group_state=state; //--- int buttons_total=ButtonsTotal(); for(int i=0; i<buttons_total; i++) { m_buttons[i].State(false); m_buttons[i].Color((state)? m_text_color : m_text_color_off); m_buttons[i].BackColor((state)? m_buttons_color[i]: m_back_color_off); m_buttons[i].BorderColor((state)? m_border_color : m_border_color_off); } //--- Нажать кнопку, если до блокировки была нажатая if(m_buttons_group_state) { if(m_selected_button_index!=WRONG_VALUE) { m_buttons_state[m_selected_button_index]=true; m_buttons[m_selected_button_index].Color(m_text_color_pressed); m_buttons[m_selected_button_index].BackColor(m_buttons_color_pressed[m_selected_button_index]); } } }
Нужен метод, с помощью которого можно переключать кнопку при нажатии. Также понадобятся поля и методы для сохранения и получения текста и индекса выделенной кнопки.
В методе переключения кнопки (см. листинг кода ниже) в самом начале стоит проверка на количество кнопок в группе. Если окажется, что кнопок вообще нет, то в журнал будет выведено сообщение с подсказкой. При этом выход из метода не будет осуществлен. Программа пойдёт дальше и столкнётся с ошибкой выхода из диапазона массива m_buttons_state[]. То есть, разработчик приложения должен самостоятельно изначально всё сделать правильно (добавив хотя бы одну кнопку в группу), чтобы такой ошибки не было. А вот уже после проверки, если в группе есть хотя бы одна кнопка, программа скорректирует переданный индекс в случае выхода из диапазона. Затем осуществляется изменение состояния кнопки по указанному индексу.
После этого в цикле все кнопки, кроме нажатой, отжимаются (устанавливается соответствующий цвет), при этом учитывается режим группы. Иными словами, если включен режим «радио-кнопки», когда хотя бы одна кнопка всегда должна быть нажата, то во время каждой итерации проходит проверка условия на равенство переданного индекса с текущим индексом цикла.
Если включен режим, когда все кнопки могут быть отжаты, кроме проверки на индекс проверяется еще и текущее состояние кнопки. Если условие исполнилось, то (независимо от режима) ставится флаг, что есть нажатая кнопка. По этому флагу в конце метода полям класса, в которых должны сохраняться текст и индекс выделенной кнопки, устанавливаются соответствующие значения. В случае, когда нет ни одной нажатой кнопки, устанавливаются пустые значения ("" и WRONG_VALUE).
class CButtonsGroup : public CElement { private: //--- (1) Текст и (2) индекс выделенной кнопки string m_selected_button_text; int m_selected_button_index; //--- public: //--- Возвращает (1) текст и (2) индекс выделенной кнопки string SelectedButtonText(void) const { return(m_selected_button_text); } int SelectedButtonIndex(void) const { return(m_selected_button_index); } //--- Переключает кнопку по указанному индексу void SelectionButton(const int index); }; //+------------------------------------------------------------------+ //| Переключает кнопку по указанному индексу | //+------------------------------------------------------------------+ void CButtonsGroup::SelectionButton(const int index) { //--- Для проверки существования нажатой в группе кнопки bool check_pressed_button=false; //--- Получим количество кнопок int buttons_total=ButtonsTotal(); //--- Если нет ни одной кнопки в группе, сообщить об этом if(buttons_total<1) { ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, " "когда в группе есть хотя бы одна кнопка! Воспользуйтесь методом CButtonsGroup::AddButton()"); } //--- Скорректировать значение индекса, если выходит из диапазона int correct_index=(index>=buttons_total)? buttons_total-1 : (index<0)? 0 : index; //--- Изменить состояние кнопки на противоположное m_buttons_state[correct_index]=(m_buttons_state[correct_index])? false : true; //--- Пройдёмся в цикле по группе кнопок for(int i=0; i<buttons_total; i++) { //--- В зависимости от режима осуществляется соответствующая проверка bool condition=(m_radio_buttons_mode)? (i==correct_index) : (i==correct_index && m_buttons_state[i]); //--- Если условие исполнено, сделаем кнопку нажатой if(condition) { if(m_radio_buttons_mode) m_buttons_state[i]=true; //--- Есть нажатая кнопка check_pressed_button=true; //--- Установить цвета m_buttons[i].Color(m_text_color_pressed); m_buttons[i].BackColor(m_buttons_color_pressed[i]); CElement::InitColorArray(m_buttons_color_pressed[i],m_buttons_color_pressed[i],m_buttons_total[i].m_buttons_color_array); } //--- Если условие не исполнилось, сделаем кнопку отжатой else { //--- Установить отключенное состояние и цвета m_buttons_state[i]=false; m_buttons[i].Color(m_text_color); m_buttons[i].BackColor(m_buttons_color[i]); CElement::InitColorArray(m_buttons_color[i],m_buttons_color_hover[i],m_buttons_total[i].m_buttons_color_array); } //--- Обнулить штатное состояние кнопки m_buttons[i].State(false); } //--- Если есть нажатая кнопка, сохраним её текст и индекс m_selected_button_text =(check_pressed_button) ? m_buttons[correct_index].Description() : ""; m_selected_button_index =(check_pressed_button) ? correct_index : WRONG_VALUE; }
Для обработки нажатия на кнопке группы создадим метод CButtonsGroup::OnClickButton(). Так же, как и во многих других одноимённых методах, рассмотренных ранее, здесь осуществляются проверки:
- по имени;
- по идентификатору. Для извлечения идентификатора из имени объекта используется метод CButtonsGroup::IdFromObjectName(). Код метода аналогичен одноимённым методам, рассмотренным ранее в других классах элементов управления, поэтому не будем здесь его приводить;
- по текущему состоянию (доступен/заблокирован).
Если все проверки пройдены и программа не вышла из метода, с помощью рассмотренного ранее метода CButtonsGroup::SelectionButton() осуществляется переключение кнопки в группе. В самом конце метода в поток событий отправляется сообщение с данными нажатой кнопки, которое можно принять в обработчике пользовательского класса.
class CButtonsGroup : public CElement { private: //--- Обработка нажатия на кнопку bool OnClickButton(const string clicked_object); //--- Получение идентификатора из имени кнопки int IdFromObjectName(const string object_name); }; //+------------------------------------------------------------------+ //| Обработчик события графика | //+------------------------------------------------------------------+ void CButtonsGroup::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработка события нажатия левой кнопки мыши на объекте if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickButton(sparam)) return; } } //+------------------------------------------------------------------+ //| Нажатие на кнопку в группе | //+------------------------------------------------------------------+ bool CButtonsGroup::OnClickButton(const string pressed_object) { //--- Выйдем, если нажатие было не на пункте меню if(::StringFind(pressed_object,CElement::ProgramName()+"_buttons_",0)<0) return(false); //--- Получим идентификатор из имени объекта int id=IdFromObjectName(pressed_object); //--- Выйдем, если идентификаторы не совпадают if(id!=CElement::Id()) return(false); //--- Для проверки индекса int check_index=WRONG_VALUE; //--- Проверим, было ли нажатие на одной из кнопок этой группы int buttons_total=ButtonsTotal(); //--- Выйти, если кнопки заблокированы if(!m_buttons_group_state) { for(int i=0; i<buttons_total; i++) m_buttons[i].State(false); //--- return(false); } //--- Если нажатие было, то запомним индекс for(int i=0; i<buttons_total; i++) { if(m_buttons[i].Name()==pressed_object) { check_index=i; break; } } //--- Выйдем, если не было нажатия на кнопку в этой группе if(check_index==WRONG_VALUE) return(false); //--- Переключить кнопку SelectionButton(check_index); //--- Отправить сигнал об этом ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),m_selected_button_index,m_selected_button_text); return(true); }
Теперь нужно настроить реакцию кнопок группы на перемещение над ними курсора мыши и нажатие левой кнопки мыши. Для этого напишем метод CButtonsGroup::CheckPressedOverButton(), который будет вызываться в обработчике элемента при обработке события CHARTEVENT_MOUSE_MOVE. Перед вызовом осуществляется несколько проверок, таких как: (1) видим ли мы сейчас элемент, (2) доступен ли он, (3) доступна ли форма, (4) нажата ли левая кнопка мыши.
class CButtonsGroup : public CElement { private: //--- Проверка нажатой левой кнопки мыши над кнопками группы void CheckPressedOverButton(void); }; //+------------------------------------------------------------------+ //| Обработчик события графика | //+------------------------------------------------------------------+ void CButtonsGroup::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработка события перемещения курсора if(id==CHARTEVENT_MOUSE_MOVE) { //--- Выйти, если элемент скрыт if(!CElement::IsVisible()) return; //--- Выйти, если кнопки заблокированы if(!m_buttons_group_state) return; //--- Определим фокус int x=(int)lparam; int y=(int)dparam; int buttons_total=ButtonsTotal(); for(int i=0; i<buttons_total; i++) { m_buttons[i].MouseFocus(x>m_buttons[i].X() && x<m_buttons[i].X2() && y>m_buttons[i].Y() && y<m_buttons[i].Y2()); } //--- Выйти, если форма заблокирована if(m_wnd.IsLocked()) return; //--- Выйти, если кнопка мыши не нажата if(sparam!="1") return; //--- Проверка нажатой левой кнопки мыши над кнопками группы CheckPressedOverButton(); return; } } //+------------------------------------------------------------------+ //| Проверка нажатой левой кнопки мыши над кнопками группы | //+------------------------------------------------------------------+ void CButtonsGroup::CheckPressedOverButton(void) { int buttons_total=ButtonsTotal(); //--- Установить цвет в зависимости от местоположения зажатой левой кнопки мыши for(int i=0; i<buttons_total; i++) { //--- Если есть фокус, то цвет нажатой кнопки if(m_buttons[i].MouseFocus()) m_buttons[i].BackColor(m_buttons_color_pressed[i]); //--- Если фокуса нет, то... else { //--- ...если кнопка группы не нажата, присвоить цвет фона if(!m_buttons_state[i]) m_buttons[i].BackColor(m_buttons_color[i]); } } }
Всё готово к тому, чтобы протестировать группу из простых кнопок в тестовом приложении. Сделаем копию эксперта, которого ранее тестировали. Удалим из него все элементы управления, кроме главного меню с его контекстными меню. Далее в статье будем использовать эту программу для тестов различных групп кнопок.
В пользовательском классе создаём экземпляр класса группы кнопок CButtonsGroup. Объявляем метод CProgram::CreateButtonsGroup1() для его создания и отступы от крайней точки формы:
class CProgram : public CWndEvents { private: //--- Группа простых кнопок CButtonsGroup m_buttons_group1; //--- private: //--- Группа простых кнопок #define BUTTONS_GROUP1_GAP_X (7) #define BUTTONS_GROUP1_GAP_Y (50) bool CreateButtonsGroup1(void); };
Создадим группу из четырёх кнопок. Расположим их горизонтально. Для примера сделаем две кнопки красного цвета, а две — синего. Имплементация метода CProgram::CreateButtonsGroup1() показана в листинге кода ниже. После создания группы кнопок, если нужно, чтобы какая-то из них была сразу выделена, воспользуйтесь публичным методом CButtonsGroup::SelectionButton().
//+------------------------------------------------------------------+ //| Создаёт группу из простых кнопкок | //+------------------------------------------------------------------+ bool CProgram::CreateButtonsGroup1(void) { //--- Сохраним указатель на окно m_buttons_group1.WindowPointer(m_window); //--- Координаты int x =m_window.X()+BUTTONS_GROUP1_GAP_X; int y =m_window.Y()+BUTTONS_GROUP1_GAP_Y; //--- Свойства int buttons_x_gap[] ={0,72,144,216}; string buttons_text[] ={"BUTTON 1","BUTTON 2","BUTTON 3","BUTTON 4"}; int buttons_width[] ={70,70,70,70}; color buttons_color[] ={C'195,0,0',C'195,0,0',clrRoyalBlue,clrRoyalBlue}; color buttons_color_hover[] ={C'255,51,51',C'255,51,51',C'85,170,255',C'85,170,255'}; color buttons_color_pressed[] ={C'135,0,0',C'135,0,0',C'50,100,135',C'50,100,135'}; //--- Установим свойства m_buttons_group1.TextColor(clrWhite); m_buttons_group1.TextColorPressed(clrGold); //--- Добавим четыре кнопки в группу for(int i=0; i<4; i++) m_buttons_group1.AddButton(buttons_x_gap[i],0,buttons_text[i],buttons_width[i], buttons_color[i],buttons_color_hover[i],buttons_color_pressed[i]); //--- Создать группу кнопок if(!m_buttons_group1.CreateButtonsGroup(m_chart_id,m_subwin,x,y)) return(false); //--- Выделим вторую кнопку в группе m_buttons_group1.SelectionButton(1); //--- Добавим объект в общий массив групп объектов CWndContainer::AddToElementsArray(0,m_buttons_group1); return(true); }
После компиляции файлов и загрузки программы на график должен получиться результат, как показано на скриншоте ниже:
Рис. 2. Тест элемента управления «Группа простых кнопок».
С первой группой кнопок мы разобрались. Готовую версию класса CButtonsGroup можно скачать в конце статьи. Следующий на очереди элемент управления – группа радио-кнопок.
Разработка класса для создания групп радио-кнопок
Создаём файл RadioButtons.mqh с классом CRadioButtons, в котором должны быть стандартные виртуальные методы и члены класса для сохранения и получения указателя на форму. Примеры можете посмотреть в классах других элементов в статье выше. Подключаем файл RadioButtons.mqh к библиотеке (WndContainer.mqh).
Каждая радио-кнопка будет собираться из трёх объектов примитивов:
- фон;
- ярлык;
- текстовая метка.
Рис. 3. Составные части радио-кнопок.
В отличие от группы простых кнопок, в группе радио-кнопок будет только один режим, так как в этом типе элемента управления не бывает ситуации, в которой одновременно отжаты все кнопки. Какая-то одна кнопка в группе будет нажата всегда. Цвет фона обычно устанавливается таким же, как цвет фона формы, к которой присоединяется группа. По сути, он используется для определения фокуса и отслеживания нажатия на радио-кнопке (повышенный приоритет). В данной версии можно настроить изменение цвета только у текстовой метки при попадании курсора в область фона кнопки и выхода из неё. Списки уникальных и общих свойств радио-кнопки отличается от списков этих свойств простой кнопки (см. перечисления ниже).
Общие свойства:
- Цвет фона и рамки
- Цвета текста
- Высота
- Ярлыки кнопки в (1) активном, (2) отключенном и (3) заблокированном состоянии
- Приоритет на нажатие левой кнопкой мыши
Уникальные свойства:
- Текст
- Ширина
- Состояние. Только одна кнопка в группе может быть нажата
- Отступы. Также, как и простые кнопки в группе, радио-кнопки можно расположить в любом порядке (мозаикой, горизонтально, вертикально и т.д.)
- Градиенты для текстовых меток
Ниже показано, как это всё выглядит в коде класса CRadioButtons. Размер массивов уникальных свойств и заполнение значениями осуществляются с помощью публичного метода CRadioButtons::AddButton() перед созданием элемента в пользовательском классе. Если ни одной кнопки не добавлено, то создание графического интерфейса программы будет прервано. Это уже было показано на примере разработки класса для создания группы простых кнопок, поэтому не будем больше на этом останавливаться подробно.
//+------------------------------------------------------------------+ //| Класс для создания группы радио-кнопок | //+------------------------------------------------------------------+ class CRadioButtons : public CElement { private: //--- Градиенты текстовых меток struct LabelsGradients { color m_labels_color_array[]; }; LabelsGradients m_labels_total[]; //--- Свойства кнопок: // (1) Цвет и (2) приоритет фона на нажатие левой кнопкой мыши color m_area_color; int m_area_zorder; //--- Массивы для уникальных свойств кнопок bool m_buttons_state[]; int m_buttons_x_gap[]; int m_buttons_y_gap[]; int m_buttons_width[]; string m_buttons_text[]; //--- Высота кнопок int m_button_y_size; //--- Ярлыки кнопки в активном, отключенном и заблокированном состоянии string m_icon_file_on; string m_icon_file_off; string m_icon_file_on_locked; string m_icon_file_off_locked; //--- Цвета текста color m_text_color; color m_text_color_off; color m_text_color_hover; //--- Приоритет на нажатие левой кнопкой мыши int m_buttons_zorder; //--- public: //--- Установка ярлыков для кнопки в активном, отключенном и заблокированном состояниях void IconFileOn(const string file_path) { m_icon_file_on=file_path; } void IconFileOff(const string file_path) { m_icon_file_off=file_path; } void IconFileOnLocked(const string file_path) { m_icon_file_on_locked=file_path; } void IconFileOffLocked(const string file_path) { m_icon_file_off_locked=file_path; } //--- (1) Цвета фона, (2) цвета текста void AreaColor(const color clr) { m_area_color=clr; } void TextColor(const color clr) { m_text_color=clr; } void TextColorOff(const color clr) { m_text_color_off=clr; } void TextColorHover(const color clr) { m_text_color_hover=clr; } //--- Добавляет кнопку с указанными свойствами до создания void AddButton(const int x_gap,const int y_gap,const string text,const int width); };
Далее нужно создать массивы экземпляров классов объектов-примитивов и методы для их создания. Также как и группа простых кнопок, радио-кнопки будут создаваться в цикле. Но здесь цикл разместим в главном методе, а индекс, который будет участвовать в формировании имени каждого объекта, будем передавать в методы создания этих объектов.
class CRadioButtons : public CElement { private: //--- Объекты для создания кнопки CRectLabel m_area[]; CBmpLabel m_icon[]; CLabel m_label[]; //--- public: //--- Методы для создания кнопки bool CreateRadioButtons(const long chart_id,const int window,const int x,const int y); //--- private: bool CreateArea(const int index); bool CreateRadio(const int index); bool CreateLabel(const int index); //--- public: //--- Количество кнопок int RadioButtonsTotal(void) const { return(::ArraySize(m_icon)); } }; //+------------------------------------------------------------------+ //| Создаёт группу объектов Кнопки | //+------------------------------------------------------------------+ bool CRadioButtons::CreateRadioButtons(const long chart_id,const int window,const int x,const int y) { //--- Выйти, если нет указателя на форму if(::CheckPointer(m_wnd)==POINTER_INVALID) { ::Print(__FUNCTION__," > Перед созданием группы радио-кнопок классу нужно передать " "указатель на форму: CButtonsGroup::WindowPointer(CWindow &object)"); return(false); } //--- Инициализация переменных m_id =m_wnd.LastId()+1; m_chart_id =chart_id; m_subwin =window; m_x =x; m_y =y; //--- Получим количество кнопок в группе int radio_buttons_total=RadioButtonsTotal(); //--- Если нет ни одной кнопки в группе, сообщить об этом if(radio_buttons_total<1) { ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, " "когда в группе есть хотя бы одна кнопка! Воспользуйтесь методом CRadioButtons::AddButton()"); return(false); } //--- Установим группу кнопок for(int i=0; i<radio_buttons_total; i++) { CreateArea(i); CreateRadio(i); CreateLabel(i); //--- Обнуление фокуса m_area[i].MouseFocus(false); } //--- Скрыть элемент, если окно диалоговое или оно минимизировано if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized()) Hide(); //--- return(true); }
Методы для изменения состояния элемента (доступен/заблокирован) и для переключения кнопки по указанному индексу в этом классе тоже должны быть. Здесь они даже немного проще, чем в классе CButtonsGroup, так как у группы радио-кнопок есть только один режим, поэтому с кодом этих методов можете ознакомиться в приложенных к статье файлах. Кроме этого, так же как и в классе для простых кнопок, необходимы методы для получения текста и индекса выделенной кнопки.
class CButtonsGroup : public CElement { private: //--- (1) Текст и (2) индекс выделенной кнопки string m_selected_button_text; int m_selected_button_index; //--- Доступен/заблокирован bool m_buttons_group_state; //--- public: //--- Общее состояние группы кнопок (доступен/заблокирован) bool ButtonsGroupState(void) const { return(m_buttons_group_state); } void ButtonsGroupState(const bool state); //--- Возвращает (1) текст и (2) индекс выделенной кнопки string SelectedButtonText(void) const { return(m_selected_button_text); } int SelectedButtonIndex(void) const { return(m_selected_button_index); } //--- Переключает кнопку по указанному индексу void SelectionButton(const int index); };
Для отправки сообщения о нажатии на элементах управления, которые визуально выглядят на форме как текстовая метка, будем применять новый идентификатор ON_CLICK_LABEL. Добавим его в файл Defines.mqh:
#define ON_CLICK_LABEL (10) // Нажатие на текстовой метке
Имплементация метода для обработки нажатия на кнопках группы представлена ниже. После прохождения всех проверок: (1) по принадлежности к типу элемента, (2) по идентификатору и (3) по доступности, далее в цикле по точному имени определяется индекс нажатой кнопки. Если было нажатие на кнопке этой группы и это кнопка, которая сейчас не выделена, осуществляется переключение и отправка сообщения, которое можно принять в пользовательском классе.
class CButtonsGroup : public CElement { private: //--- Обработка нажатия на кнопку bool OnClickButton(const string pressed_object); //--- Получение идентификатора из имени радио-кнопки int IdFromObjectName(const string object_name); }; //+------------------------------------------------------------------+ //| Обработчик события графика | //+------------------------------------------------------------------+ void CRadioButtons::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Обработка события нажатия левой кнопки мыши на объекте if(id==CHARTEVENT_OBJECT_CLICK) { //--- Переключить кнопку if(OnClickButton(sparam)) return; } } //+------------------------------------------------------------------+ //| Нажатие на радио-кнопку | //+------------------------------------------------------------------+ bool CRadioButtons::OnClickButton(const string pressed_object) { //--- Выйдем, если нажатие было не на пункте меню if(::StringFind(pressed_object,CElement::ProgramName()+"_radio_area_",0)<0) return(false); //--- Получим идентификатор и индекс из имени объекта int id=IdFromObjectName(pressed_object); //--- Выйдем, если нажали не на пункте, к которому это контекстное меню привязано if(id!=CElement::Id()) return(false); //--- Для проверки индекса int check_index=WRONG_VALUE; //--- Выйти, если кнопки заблокированы if(!m_radio_buttons_state) return(false); //--- Если нажатие было, то запомним индекс int radio_buttons_total=RadioButtonsTotal(); for(int i=0; i<radio_buttons_total; i++) { if(m_area[i].Name()==pressed_object) { check_index=i; break; } } //--- Выйдем, если не было нажатия на кнопку в этой группе или // если это уже выделенная радио-кнопка if(check_index==WRONG_VALUE || check_index==m_selected_button_index) return(false); //--- Переключить кнопку SelectionRadioButton(check_index); //--- Отправить сигнал об этом ::EventChartCustom(m_chart_id,ON_CLICK_LABEL,CElement::Id(),check_index,m_selected_button_text); return(true); }
Всё готово для тестов. Добавим в эксперта, в котором тестировали ранее группу из простых кнопок, четыре группы из радио-кнопок (CRadioButtons) и ещё одну группу из простых кнопок (CButtonsGroup).
class CProgram : public CWndEvents { private: //--- Группа радио-кнопок 1 CRadioButtons m_radio_buttons1; //--- Группа простых кнопок 2 CButtonsGroup m_buttons_group2; //--- Группы радио-кнопок 2,3,4 CRadioButtons m_radio_buttons2; CRadioButtons m_radio_buttons3; CRadioButtons m_radio_buttons4; //--- private: //--- Группа радио-кнопок 1 #define RADIO_BUTTONS1_GAP_X (7) #define RADIO_BUTTONS1_GAP_Y (75) bool CreateRadioButtons1(); //--- Группа простых кнопок 2 #define BUTTONS_GROUP2_GAP_X (7) #define BUTTONS_GROUP2_GAP_Y (100) bool CreateButtonsGroup2(void); //--- Группы радио-кнопок 2,3,4 #define RADIO_BUTTONS2_GAP_X (7) #define RADIO_BUTTONS2_GAP_Y (125) bool CreateRadioButtons2(); #define RADIO_BUTTONS3_GAP_X (105) #define RADIO_BUTTONS3_GAP_Y (125) bool CreateRadioButtons3(); #define RADIO_BUTTONS4_GAP_X (203) #define RADIO_BUTTONS4_GAP_Y (125) bool CreateRadioButtons4(); }; //+------------------------------------------------------------------+ //| Создаёт торговую панель | //+------------------------------------------------------------------+ bool CProgram::CreateTradePanel(void) { //--- Создание формы для элементов управления //--- Создание элементов управления: // Главное меню //--- Контекстные меню //--- Группа простых кнопок 1 //--- Группа радио-кнопок 1 if(!CreateRadioButtons1()) return(false); //--- Группа простых кнопок 2 if(!CreateButtonsGroup2()) return(false); //--- Группы радио-кнопок 2,3,4 if(!CreateRadioButtons2()) return(false); if(!CreateRadioButtons3()) return(false); if(!CreateRadioButtons4()) return(false); //--- Перерисовка графика m_chart.Redraw(); return(true); }
Для примера приведём имплементацию метода одного из них. Все остальные могут отличаться только значениями своих параметров.
//+------------------------------------------------------------------+ //| Создаёт группу радио-кнопок 1 | //+------------------------------------------------------------------+ bool CProgram::CreateRadioButtons1(void) { //--- Передать объект панели m_radio_buttons1.WindowPointer(m_window); //--- Координаты int x =m_window.X()+RADIO_BUTTONS1_GAP_X; int y =m_window.Y()+RADIO_BUTTONS1_GAP_Y; //--- Свойства int buttons_x_offset[] ={0,98,196}; int buttons_y_offset[] ={0,0,0}; string buttons_text[] ={"Radio Button 1","Radio Button 2","Radio Button 3"}; int buttons_width[] ={92,92,92}; //--- for(int i=0; i<3; i++) m_radio_buttons1.AddButton(buttons_x_offset[i],buttons_y_offset[i],buttons_text[i],buttons_width[i]); //--- Создать группу кнопок if(!m_radio_buttons1.CreateRadioButtons(m_chart_id,m_subwin,x,y)) return(false); //--- Выделим вторую кнопку в группе m_radio_buttons1.SelectedRadioButton(1); //--- Заблокируем радио-кнопки m_radio_buttons1.RadioButtonsState(false); //--- Добавим объект в общий массив групп объектов CWndContainer::AddToElementsArray(0,m_radio_buttons1); return(true); }
В дополнительной (во второй) группе кнопок типа CButtonsGroup сделаем две кнопки. Для примера реализуем всё так, что вторая кнопка этой группы будет блокировать первые группы простых и радио-кнопок, а первая будет делать их доступными.
//+------------------------------------------------------------------+ //| Обработчик событий | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Событие нажатия на кнопке if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam); //--- Если идентификатор от второй группы простых кнопок и // текст выделенной кнопки в этой группе совпадает со строковым параметром сообщения if(lparam==m_buttons_group2.Id() && sparam==m_buttons_group2.SelectedButtonText()) { //--- Если индекс первой кнопки, разблокируем указанные элементы if((int)dparam==0) { m_buttons_group1.ButtonsGroupState(true); m_radio_buttons1.RadioButtonsState(true); } //--- Если индекс второй кнопки, заблокируем указанные элементы else { m_buttons_group1.ButtonsGroupState(false); m_radio_buttons1.RadioButtonsState(false); } } return; } }
Скомпилируйте все файлы и загрузите программу на график. На данном этапе разработки должен получиться результат, как на скриншоте ниже:
Рис. 4. Тест элемента «Радио-кнопки».
Разработка класса для создания элемента «Группа радио-кнопок» завершена. Полную версию Вы можете скачать в приложенных к статье файлах.
Разработка класса для создания групп кнопок с картинками
Ранее был создан класс CIconButton для создания элемента «Кнопка с картинкой». Сейчас реализуем элемент управления, который позволит создавать группу таких кнопок. Уже в привычной папке Controls, где расположены другие созданные ранее файлы с классами элементов управления, создайте файл IconButtonsGroup.mqh с классом CIconButtonsGroup, в котором сразу можно объявить и имплементировать стандартные виртуальные методы. Подключите этот файл к библиотеке (файл WndContainer.mqh).
На этот раз не будем всё подробно описывать, так как на самом деле класс CIconButtonsGroup — это симбиоз из всех тех методов, которые уже были рассмотрены ранее в классах CButtonsGroup и CRadioButtons. Так же, как и в классе группы радио-кнопок (CRadioButtons), объекты, из которых собирается каждая кнопка группы, будут создаваться приватными методами в цикле главного метода создания элемента. Единственным параметром этих методов является индекс, который будет использоваться в формировании имени графических объектов. При наведении курсора у кнопок этой группы можно настроить изменение текста и цвета фона.
Методы, которые относятся к обработке событий, уже описывались в этой статье. В этом типе группы кнопок они практически такие же, поэтому приведём лишь содержание класса CIconButtonsGroup (см. листинг кода ниже). Имплементацию методов, которые вынесены за пределы тела класса, можно посмотреть в приложенных к статье файлах.
//+------------------------------------------------------------------+ //| Класс для создания группы кнопок с картинками | //+------------------------------------------------------------------+ class CIconButtonsGroup : public CElement { private: //--- Указатель на форму, к которой элемент присоединён CWindow *m_wnd; //--- Объекты для создания кнопки CButton m_buttons[]; CBmpLabel m_icons[]; CLabel m_labels[]; //--- Градиенты текстовых меток struct IconButtonsGradients { color m_back_color_array[]; color m_label_color_array[]; }; IconButtonsGradients m_icon_buttons_total[]; //--- Свойства кнопок: // Массивы для уникальных свойств кнопок bool m_buttons_state[]; int m_buttons_x_gap[]; int m_buttons_y_gap[]; string m_buttons_text[]; int m_buttons_width[]; string m_icon_file_on[]; string m_icon_file_off[]; //--- Высота кнопок int m_buttons_y_size; //--- Цвет фона в различных режимах color m_back_color; color m_back_color_off; color m_back_color_hover; color m_back_color_pressed; //--- Цвет рамки color m_border_color; color m_border_color_off; //--- Отступы ярлыка int m_icon_x_gap; int m_icon_y_gap; //--- Текст и отступы текстовой метки int m_label_x_gap; int m_label_y_gap; //--- Цвет текстовой метки в различных режимах color m_label_color; color m_label_color_off; color m_label_color_hover; color m_label_color_pressed; //--- (1) Текст и (2) индекс выделенной кнопки string m_selected_button_text; int m_selected_button_index; //--- Общий приоритет для некликабельных объектов int m_zorder; //--- Приоритет на нажатие левой кнопкой мыши int m_buttons_zorder; //--- Доступен/заблокирован bool m_icon_buttons_state; //--- public: CIconButtonsGroup(void); ~CIconButtonsGroup(void); //--- Методы для создания кнопки bool CreateIconButtonsGroup(const long chart_id,const int window,const int x,const int y); //--- private: bool CreateButton(const int index); bool CreateIcon(const int index); bool CreateLabel(const int index); //--- public: //--- (1) Сохраняет указатель формы, (2) высота кнопок, (3) количество кнопок, // (4) общее состояние кнопки (доступен/заблокирован) void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } void ButtonsYSize(const int y_size) { m_buttons_y_size=y_size; } int IconButtonsTotal(void) const { return(::ArraySize(m_icons)); } bool IconButtonsState(void) const { return(m_icon_buttons_state); } void IconButtonsState(const bool state); //--- Цвета фона кнопки void BackColor(const color clr) { m_back_color=clr; } void BackColorOff(const color clr) { m_back_color_off=clr; } void BackColorHover(const color clr) { m_back_color_hover=clr; } void BackColorPressed(const color clr) { m_back_color_pressed=clr; } //--- Отступы ярлыка void IconXGap(const int x_gap) { m_icon_x_gap=x_gap; } void IconYGap(const int y_gap) { m_icon_y_gap=y_gap; } //--- Отступы текстовой метки void LabelXGap(const int x_gap) { m_label_x_gap=x_gap; } void LabelYGap(const int y_gap) { m_label_y_gap=y_gap; } //--- Возвращает (1) текст и (2) индекс выделенной кнопки string SelectedButtonText(void) const { return(m_selected_button_text); } int SelectedButtonIndex(void) const { return(m_selected_button_index); } //--- Переключает радио-кнопку по указанному индексу void SelectedRadioButton(const int index); //--- Добавляет кнопку с указанными свойствами до создания void AddButton(const int x_gap,const int y_gap,const string text, const int width,const string icon_file_on,const string icon_file_off); //--- Изменение цвета void ChangeObjectsColor(void); //--- public: //--- Обработчик событий графика virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam); //--- Таймер virtual void OnEventTimer(void); //--- Перемещение элемента virtual void Moving(const int x,const int y); //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление virtual void Show(void); virtual void Hide(void); virtual void Reset(void); virtual void Delete(void); //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши virtual void SetZorders(void); virtual void ResetZorders(void); //--- private: //--- Обработка нажатия на кнопку bool OnClickButton(const string pressed_object); //--- Проверка нажатой левой кнопки мыши над кнопками группы void CheckPressedOverButton(void); //--- Получение идентификатора из имени радио-кнопки int IdFromObjectName(const string object_name); };
Далее протестируем этот элемент управления. Для примера создадим одну группу кнопок с картинками на форме того же эксперта, в котором тестировали ранее группы кнопок типа CButtonsGroup и CRadioButtons. Для этого в пользовательский класс добавьте строки кода, как это показано в листинге ниже.
class CProgram : public CWndEvents { private: //--- Группа кнопок с картинками 1 CIconButtonsGroup m_icon_buttons_group1; //--- private: //--- Группа кнопок с картинками 1 #define IBUTTONS_GROUP1_GAP_X (7) #define IBUTTONS_GROUP1_GAP_Y (190) bool CreateIconButtonsGroup1(void); }; //+------------------------------------------------------------------+ //| Создаёт торговую панель | //+------------------------------------------------------------------+ bool CProgram::CreateTradePanel(void) { //--- Создание формы для элементов управления //--- Создание элементов управления: // Главное меню //--- Контекстные меню //--- Группа простых кнопок 1 //--- Группа радио-кнопок 1 //--- Группа простых кнопок 2 //--- Группы радио-кнопок 2,3,4 //--- Группа кнопок с картинками 1 if(!CreateIconButtonsGroup1()) return(false); //--- Перерисовка графика m_chart.Redraw(); return(true); }
Скомпилируйте файлы и загрузите программу на график. Должен получиться результат, как показано на скриншоте ниже:
Рис. 5. Тест группы кнопок с картинками.
Разработка класса для создания элемента «Группа кнопок с картинками» завершена. Полную версию вы можете скачать в приложенных к статье файлах.
Заключение
Мы закончили третью часть серии статей о разработке библиотеки для создания графических интерфейсов в торговых терминалах MetaTrader. На текущем этапе разработки схема библиотеки выглядит так:
Рис. 6. Структура библиотеки на текущей стадии разработки.
В следующей (четвёртой) части продолжим развивать библиотеку и рассмотрим следующие темы:
- Многооконный режим
- Система управления приоритетами на нажатие левой кнопкой мыши на графических объектах
- Информационные элементы интерфейса «Статусная строка» и "Всплывающие подсказки"
Ниже вы можете загрузить на свой компьютер архивы с файлами библиотеки на текущей стадии разработки, изображения и файлы рассматриваемых в статье программ для тестов в терминалах MetaTrader. Если у Вас возникают вопросы по использованию материала предоставленного в этих файлах, то Вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей в представленном списке ниже или задать вопрос в комментариях к статье.
Список статей (глав) третьей части:
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
При компиляции файла SplitButton.mqh выдает ошибку "'return' - cannot convert from const pointer to nonconst pointer SplitButton.mqh 90 65"
Да, после последнего обновления терминала появилась такая ошибка. Правила "игры" немного изменились. Исправить можно просто удалив спецификатор const.
Перейдите к строке с ошибкой и замените эту строку:
На эту:
//---
Подобные исправления нужно будет внести во всех файлах, где будет встречаться такая ошибка. В следующих статьях серии ошибка будет устранена.
Спасибо за сообщение.