Продолжение. Начало - здесь: Динамическая обработка событий
В предыдущих разделах объясняется, как определить обработчики событий, но не объясняется, как именно WxWidgets находит обработчика для данного события. Рассмотрим этот алгоритм.
В процессе рассмотрения для лучшего понимания можно загрузить пример event, отсюда: https://github.com/wxWidgets/wxWidgets/tree/master/samples/event
В процессе рассмотрения для лучшего понимания можно загрузить пример event, отсюда: https://github.com/wxWidgets/wxWidgets/tree/master/samples/event
Когда событие приходит из оконной системы, wxWidgets вызывает wxEvtHandler::ProcessEvent() в первом объекте - обработчике события, принадлежащего окну, сгенерировшему событие. Обычный порядок поиска в таблице событий, выполняемый ProcessEvent(), таков: обработка события прекращается, как только находится обработчик (если обработчик не вызовет wxEvent::Skip(), в этом случае считается, что событие не обработано, и поиск продолжается):
- В самом начале вызывается, wxApp::FilterEvent(). Если метод вернет что-либо, кроме -1 (по умолчанию), обработка немедленно останавливается.
- Если объект есть wxWindow и имеет связанный валидатор, wxValidator получает шанс обработать событие.
- Если обработчик отвегрнут путем вызова wxEvtHandler::SetEvtHandlerEnabled(), следующие два шага пропускаются, и обработчик события продолжает работу с шага (5).
- Просматривается список динамически назначенных обработчиков, то есть тех, для которых были вызваны с помощью Bind<>(). Заметим, что это делается перед просмотром записей статической таблицы событий, таким образом если и динамический и статический обработчик привязаны к одному событию, статический обработчик никогда не будет вызван, за исключением случая, когда в динамическом обработчике будет вызван метод wxEvent::Skip(). Также отметим, что динамически связанные обработчики ищутся в порядке их регистрации во время выполнения программы, то есть позже назначенные обработчики имеют приоритет над предыдущими (стек?).
- Исследуется таблица событий, содержащая все обработчики, определенные с помощью таблицы макросов в данном классе и его родительских классах. Поиск в таблице событий выполняется в том же порядке, в котором макросы событий появляются в исходном коде, то есть более ранние элементы имеют приоритет над последующими. Отметим, то сие означает, что любые обработчики событий, определенные в родительском классе, будут обработаны в данном шаге.
- Событие передается следующему обработчику, если он есть в цепочке обработчиков, то есть, для этого выполняются шаги с (1) по (4). Обычно следующего обработчика событий нет, поэтому управление передается в следующий шаг, однако прочитайте все же про Цепочку обработчиков событий, чтобы знать, как следующий может быть определен следующий обработчик.
- Если объект является wxWindow, и событие настроено для распространения (по умолчанию только события - наследники от wxCommandEvent настроены для распространения), то для родительского окна обработка перезапускается с шага (1) (и исключая шаг (7)). Если объект не является окном, но имеется следующий обработчик, событие передается его родителю, если это окно. Это гарантирует, что в общем случае (возможно, для нескольких) для не-оконных обработчиков событий, расположенных поверх окна, что события в итоге достигнут родительское окно.
- В итоге, например, если событие все еще не обработано, сам объект wxApp (который унаследован от wxEvtHandler) получает последний шанс обработать это событие.
Пожалуйста, заострите внимание на шаге 6! Люди часто упускают из виду или тупо путаются в мощных возможностях системы обработки событий wxWidgets. В следующем разделе расписаны подробности распространения событий по иерархии окон.
Также отметьте, что в обработке событий есть дополнительные шаги в части оконного взаимодействия во фреймворке wxWidgets document-view, например, в wxDocParentFrame, wxDocChildFrame и их MDI эквивалентах wxDocMDIParentFrame и wxDocMDIChildFrame. Родительские классы окон изменяют шаг (2), чтобы сначала отправить события, принятые ими, объекту wxDocManager. Этот объект, в свою очередь, отправляет событие в текущее представление, а само представление позволяет ассоциировать документ, обрабатывающий событие первым. Классы дочернего окна отправляют событие напрямую ассоциированному представлению, которое по-прежнему отправляет его объекту документа. Обратите внимание, что, чтобы избежать запоминания точного порядка обработки событий в окне документа, самым простым и рекомендуемым решением является обработка событий только на уровне классов представления, а не в классах документа или менеджера документов.
Как события распространяются вверх
Как было упомянуто ранее, события классов, унаследованных от wxCommandEvent, по умолчанию распространяются на родительское окно, если они не обрабатываются в самом окне. Но хотя по умолчанию распространяются лишь события - команды вроде упомянутого, другие события тоже могут быть распространены, так как код обработки событий использует wxEvent::ShouldPropagate(), чтобы определить, должно ли проходить распространение событий. Также возможно распространить событие только ограниченное число раз, а не до тех пор, пока оно будет обработано (или достигнет родительского окна верхнего уровня).
Наконец, есть еще одно дополнительное усложнение (которое фактически значительно упрощает жизнь программистам wxWidgets): при распространении командных событий вверх к родительскому окну, распространение события прекращается, когда достигает родительского диалога, если таковой имеется. Это означает, что вы не рискуете получить неожиданное событие от контролов диалога (которые могут быть оставлены необработанными диалогом потому, что он об этом не позаботился) при показе модальных диалогов. Однако, события распространяются за границы окон. Обоснованием такого решения является то, что в типичном приложении лишь несколько окон, и их дочерне-родительские отношения хорошо понятны для программиста, в то же время это может быть сложно или даже невозможно для для обработки всех диалогов, которые могут быть показаны в сложных приложениях (вспомним, что некоторые из них автоматически создаются самой wxWidgets). Если вы хотите определить другое поведение вы можете использовать
wxWindow::SetExtraStyle(wxWS_EX_BLOCK_EVENTS), чтобы явно запретить распространение событий за пределами данного окна, или отключить этот флаг для диалогов, который установлен у них по умолчанию.
Обычно события, которые происходят с окном как именно с окном (изменение размера, перемещение, отрисовка, мышь, клавиатура. и т.д.), посылаются только к окну. События, которые имеют более высокий уровень, или генерируются самим окном (нажатие на кнопку, выбор пункта меню, разворачивание ветви дерева,и т.д.) являются командными событиями и посылаются родителю, чтобы узнать, интересуется ли он этими событиями. Точнее, как сказано выше, все классы событий, не связанные с wxCommandEvent (см. Диаграмму наследования wxEvent ), не распространяются вверх.
В некоторых случаях желательно получать определенное некоторые из системных событий в родительском окне, например, все ключевые события, отправленные, но не обработанные встроенными элементами управления в диалоговом окне. В этом случае должен быть написан специальный обработчик событий, который переопределит ProcessEvent(), чтобы передать все события (или любые из них на выбор) в родительское окно.
Цепочка обработчиков событий
Шаг 4 алгоритма распространения событий проверяет следующий обработчик в цепочке обработчиков событий. Эта цепочка может быть сформирована с помощью wxEvtHandler :: SetNextHandler () :
(пояснение к картинке: если вызван
A->ProcessEvent,а обработчик события отсутствует, будет вызван
B->ProcessEvent, и так далее
...).
Кроме того, в случае wxWindow мы можем создать стек (двунаправленный список, реализованный с помощью wxEvtHandler) , с помощью wxWindow::PushEventHandler():
(пояснение к картинке: если вызван
W->ProcessEvent
, он немедленно вызывает A->ProcessEvent
; если ни A, ни
B
не обрабатывают событие, то wxWindow получает его само – т.е. в качестве последней возможности используются динамически назначенные обработчики событий и статические элементы таблицы событий окна wxWindow, после проверки всех обработчиков событий, помещенных в стек).
По умолчанию цепочка пуста, то есть следующий обработчик отсутствует.
Что в итоге
Общие принципы
Так как каждое событие уникальным образом идентифицируется его типом, определение нового события начинаем с определения нового типа события. Это делается с помощью макроса wxDEFINE_EVENT(). Так как событие типа есть переменная, при необходимости она также может быть объявлена с помощью макроса wxDECLARE_EVENT().
Далее нужно решить: нужно для нового типа события определять новый класс события, или можно обойтись существующими, обычно выбор стоит между классом wxEvent (которые не содержит никакой дополнительной информации) и классом wxCommandEvent (который содержит несколько дополнительных полей, а также по умолчанию распространяется вверх). Оба варианта подробно расписаны ниже. См. также пример Event Sample, содержащий подробный код определения собственного типа события и работу с ним.
В итоге, нам нужно генерировать и оправлять пользовательские события. Генерация представляет собой просто создание экземпляра класса вашего с инициализацией его внутренних полей. Для отправки события нужному обработчику можно воспользоваться двумя способами: с помощью wxEvtHandler::AddPendingEvent или с помощью wxEvtHandler::QueueEvent. В основном последний вариант используется при межнитевой коммуникации. А если используется только "главная" нить, то спокойно можно использовать первый вариант. Наконец, следует обратить внимание на две простые функции-"обертки", ассоциированные с двумя упомянутыми функциями wxEvtHandler: функция wxPostEvent() и wxQueueEvent().
Использование существующих классов событий
Если с новым типом событий мы хотим использовать просто wxCommandEvent, то используем один из макросов таблицы общих событий, перечисленных ниже, без необходимости определять новый класс событий самостоятельно. Пример:
// Такой код обычен для заголовочного файла: просто объявляем тип события MY_EVENT
wxDECLARE_EVENT(MY_EVENT, wxCommandEvent);
// Это определение, следовательно, оно не может быть в заголовочном файле
wxDEFINE_EVENT(MY_EVENT, wxCommandEvent);
// Пример кода обработки событий с помощью таблицы событий
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU (wxID_EXIT, MyFrame::OnExit)
...
EVT_COMMAND (ID_MY_WINDOW, MY_EVENT, MyFrame::OnMyEvent)
void MyFrame::OnMyEvent(wxCommandEvent& event)
{
// Что-то длеаем
wxString text = event.GetString();
}
// Пример кода обработки события с помощью Bind<>():
MyFrame::MyFrame()
{
Bind(MY_EVENT, &MyFrame::OnMyEvent, this, ID_MY_WINDOW);
}
// Пример генерации события
void MyWindow::SendEvent()
{
wxCommandEvent event(MY_EVENT, GetId()); // Создание события
event.SetEventObject(this); // Настройка
// Наполняем каким-то содержанием
event.SetString("Hello");
// Оправляем
ProcessWindowEvent(event);
}
// Такой код обычен для заголовочного файла: просто объявляем тип события MY_EVENT
wxDECLARE_EVENT(MY_EVENT, wxCommandEvent);
// Это определение, следовательно, оно не может быть в заголовочном файле
wxDEFINE_EVENT(MY_EVENT, wxCommandEvent);
// Пример кода обработки событий с помощью таблицы событий
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU (wxID_EXIT, MyFrame::OnExit)
...
EVT_COMMAND (ID_MY_WINDOW, MY_EVENT, MyFrame::OnMyEvent)
void MyFrame::OnMyEvent(wxCommandEvent& event)
{
// Что-то длеаем
wxString text = event.GetString();
}
// Пример кода обработки события с помощью Bind<>():
MyFrame::MyFrame()
{
Bind(MY_EVENT, &MyFrame::OnMyEvent, this, ID_MY_WINDOW);
}
// Пример генерации события
void MyWindow::SendEvent()
{
wxCommandEvent event(MY_EVENT, GetId()); // Создание события
event.SetEventObject(this); // Настройка
// Наполняем каким-то содержанием
event.SetString("Hello");
// Оправляем
ProcessWindowEvent(event);
}
Определение собственного класса события
При определенных обстоятельствах бывает нужно определить собственный класс события, например, для оправки достаточно сложных данных из одного места в другое. Помимо определения такого класса, также нужно определить собственный макрос события для таблицы событий(если для обработки событий такого типа мы будем использовать таблицу событий).
Пример:
// Определение нового класса событий
{
public:
: wxEvent(winid, eventType),
m_pos(pos)
{
}
// Геттеры
// Чисто виртуальная реализация базового класса
private:
const wxPoint m_pos;
};
// Определяем один тип события MY_PLOT_CLICKED, ассоциированный с классом MyPlotEvent.
// Однако, бывает больше одного типа событий, например,
// у нас есть MY_PLOT_ZOOMED или MY_PLOT_PANNED и так далее, в этом случае
// мы просто добавляем больше строк, подобных этой:
wxDEFINE_EVENT(MY_PLOT_CLICKED, MyPlotEvent);
// Если мы хотим поддерживать старые компиляторы, то нам придется использовать
// несколько уродливых макросов:
typedef void (wxEvtHandler::*MyPlotEventFunction)(MyPlotEvent&);
#define MyPlotEventHandler(func) wxEVENT_HANDLER_CAST(MyPlotEventFunction, func)
// ...а если мы кодируем только в современных системах, то вместо этого будет достаточно
// просто делать так:
#define MyPlotEventHandler(func) (&func)
// Окончательно определим макрос события нового типа для использования в таблице событий.
//
// Запомним, что все это не нужно делать, если мы используем только Bind<>(), и
// можно заменить MyPlotEventHandler(func) на &func. Конечно, если компилятор не очень старый.
#define MY_EVT_PLOT_CLICK(id, func) \
wx__DECLARE_EVT1(MY_PLOT_CLICKED, id, MyPlotEventHandler(func))
// Пример кода обработки событий (конечно, мы будем использовать один из методов, а не оба)
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_PLOT(ID_MY_WINDOW, MyFrame::OnPlot)
MyFrame::MyFrame()
{
Bind(MY_PLOT_CLICKED, &MyFrame::OnPlot, this, ID_MY_WINDOW);
}
void MyFrame::OnPlot(MyPlotEvent& event)
{
... что-то делаем с event.GetPoint() ...
}
// Пример кода генерации события
void MyWindow::SendEvent()
{
MyPlotEvent event(MY_PLOT_CLICKED, GetId(), wxPoint(...));
event.SetEventObject(this);
ProcessWindowEvent(event);
}
Пример:
// Определение нового класса событий
{
public:
: wxEvent(winid, eventType),
m_pos(pos)
{
}
// Геттеры
// Чисто виртуальная реализация базового класса
private:
const wxPoint m_pos;
};
// Определяем один тип события MY_PLOT_CLICKED, ассоциированный с классом MyPlotEvent.
// Однако, бывает больше одного типа событий, например,
// у нас есть MY_PLOT_ZOOMED или MY_PLOT_PANNED и так далее, в этом случае
// мы просто добавляем больше строк, подобных этой:
wxDEFINE_EVENT(MY_PLOT_CLICKED, MyPlotEvent);
// Если мы хотим поддерживать старые компиляторы, то нам придется использовать
// несколько уродливых макросов:
typedef void (wxEvtHandler::*MyPlotEventFunction)(MyPlotEvent&);
#define MyPlotEventHandler(func) wxEVENT_HANDLER_CAST(MyPlotEventFunction, func)
// ...а если мы кодируем только в современных системах, то вместо этого будет достаточно
// просто делать так:
#define MyPlotEventHandler(func) (&func)
// Окончательно определим макрос события нового типа для использования в таблице событий.
//
// Запомним, что все это не нужно делать, если мы используем только Bind<>(), и
// можно заменить MyPlotEventHandler(func) на &func. Конечно, если компилятор не очень старый.
#define MY_EVT_PLOT_CLICK(id, func) \
wx__DECLARE_EVT1(MY_PLOT_CLICKED, id, MyPlotEventHandler(func))
// Пример кода обработки событий (конечно, мы будем использовать один из методов, а не оба)
wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_PLOT(ID_MY_WINDOW, MyFrame::OnPlot)
MyFrame::MyFrame()
{
Bind(MY_PLOT_CLICKED, &MyFrame::OnPlot, this, ID_MY_WINDOW);
}
void MyFrame::OnPlot(MyPlotEvent& event)
{
... что-то делаем с event.GetPoint() ...
}
// Пример кода генерации события
void MyWindow::SendEvent()
{
MyPlotEvent event(MY_PLOT_CLICKED, GetId(), wxPoint(...));
event.SetEventObject(this);
ProcessWindowEvent(event);
}
Разные замечания
Обработчики событий против виртуальных методов
Легко заметить, что обработка событий wxWidgets по духу реализована довольно схоже с механизмом виртуальных методов обычного C++: оба механизма позволяют изменять поведение базового класса путем определения функции обработчика события в классе-наследнике.
Тем не менее, между этими двумя механизмами существует важная разница применительно к случаю, когда нужно вызвать поведение по умолчанию, реализованное в базовом классе, из класса-наследника. С виртуальными функциями нужно вызвать функцию базового класса напрямую, и это можно либо в начале (для пост-обработки события) функции-обработчике в производном классе, либо в конце (для пре-обработки события). С обработчиками событий возможна только пре-обработка событий, а для того, чтобы оставить обработку по умолчанию, нужно вызвать wxEvent::Skip(), и НЕ вызывать обработчик базового класса напрямую. Фактически, обработчик событий в базовом классе скорее всего и не существует, поскольку поведение по умолчанию часто реализуется в платформозависимом коде и использованием средств операционной системы или библиотеки нижнего уровня. Но даже если обработчик существует, на уровне кода wxWidgets, он никогда не должен вызываться напрямую, поскольку обработчики событий не являются частью wxWidgets API и не должны никогда вызываться напрямую.
Пользовательские события против программно сгенерированных событий
Несмотря на то, что события wxEvents могут быть сгенерированы как действиями пользователя (например, изменением размеров wxWindow), так и вызовом функций (например, wxWindow::SetSize), контролы wxWidgets обычно посылают производные от wxCommandEvent события только сгенерированные пользователем. Исключения из этого правила::
- wxNotebook::AddPage: Нет варианта без событий.
- wxNotebook::AdvanceSelection: Нет варианта без событий.
- wxNotebook::DeletePage: Нет варианта без событий.
- wxNotebook::SetSelection: Вместо этого используем wxNotebook::ChangeSelection, так как wxNotebook::SetSelection устарел
- wxTreeCtrl::Delete: Нет варианта без событий.
- wxTreeCtrl::DeleteAllItems: Нет варианта без событий.
- wxTreeCtrl::EditLabel: Нет варианта без событий.
- Все методы wxTextCtrl
wxTextCtrl :: ChangeValue может использоваться вместо wxTextCtrl :: SetValue, но для других функций, таких как wxTextCtrl :: Replace или wxTextCtrl :: WriteText, эквивалентных событий нет.
Подключаемые обработчики событий
TODO: Возможно, уже не рекомендуются, применение Bind() решает эту задачу лучше.
Фактически, нет нужды наследовать новый класс от класса окна, если вы его не хотите. Вместо этого, можно наследовать новый класс от wxEvtHandler, определив подходящую таблицу событий, а затем вызвав wxWindow::SetEventHandler (или, что предпочтительно, wxWindow::PushEventHandler), чтобы обработчик события обработал объект, который отвечает на события. Таким способом можно избежать множества созданий наследников класса, и использовать экземпляр того же класса обработчика событий (но разные объекты не должны использоваться более чем однократно для обработки одного и того же события) для обработки событий от экземпляров различных классов контролов.
Если вам когда-либо нужно вызывать оконный обработчик событий вручную, нужно использовать функцию GetEventHandler для получения обработчика события окна и использования этого обработчика для вызова функции-члена. По умолчанию, GetEventHandler возвращает указатель на само окно, если приложение не перенаправляет события с помощью SetEventHandler или PushEventHandler.
Однократный вызов PushEventHandler временно меняет поведение GUI. Например, вы хотите захотеть вызвать в приложении диалоговый редактор, который изменяет свойства диалогов. Вы можете захватить весь ввод от существующих диалоговых окон и редактировать его "по месту", прежде чем чем восстановите поведение к нормальному состоянию. Таким образом, даже если приложение имеет новые унаследованные классы для настройки поведения, ваша утилита может обеспечить в некотором роде хак. Это может быть полезной техникой для онлайн-обучалок, где вы проводите пользователя через серьезные шаги и вы не хотите, чтобы шаги существенно отклонялись от урока. Здесь можно просматривать события, поступающие от кнопок и окон и, если это приемлемо, передавать их оригинальному обработчику событий. Для формирования цепочки обработчиков событий следует использовать PushEventHandler/PopEventHandler, где каждый обработчик обрабатывает свой диапазон событий, независимый от событий, обрабатываемых другими обработчиками.
Идентификаторы окон
Идентификаторы окон представляют собой целые числа, они одновременно используются для однозначного определения идентификатора окна в системе событий (хотя вы можете использовать их для своих собственных целей). Фактически, идентификаторы не должны быть уникальными в рамках всего приложения, поскольку они уникальны в рамках контекста , в котором вы заинтересованы, например - в окне и его дочерних элементах. К примеру, можно использовать идентификатор
wxID_OK в любом количестве диалогов, если у вас нет таких же в одном диалоговом окне
.
Если вы передадите конструктору окна значение
wxID_ANY, идентификатор будет сгенерирован автоматически системой
wxWidgets. Это бывает полезно, когда вам не нужно точно знать значение идентификатора, потому что вы не собираетесь обрабатывать события из всех всех контролов в одном месте (в этом случае в таблице событий следует указывать идентификатор wxID_ANY
или в вызове wxEvtHandler::Bind). Автоматически сгенерированные идентификаторы всегда отрицательные, поэтому их значения никогда не будут конфликтовать в идентификаторами, определенными пользователями, которые должны быть всегда положительными.
См. Список стандартных идентификаторов. Можно использовать значение wxID_HIGHEST, как значение, больше которого можно безопасно задавать собственные значения. Или использовать значения, меньшие чем wxID_LOWEST. В конце концов, значение идентификаторов можно задавать динамически, с помощью функции wxNewId(). Если в приложении последовательно использовать wxNewId(), можно быть уверенным, что идентификаторы ни счем не конфликтуют..
Общие макросы таблицы событий
EVT_CUSTOM(event, id, func) | Позволяет добавить запись в таблицу событий путем указания идентификатора события (такого как wxEVT_SIZE), идентификатор окна и функцию-член для вызова. |
EVT_CUSTOM_RANGE(event, id1, id2, func) | То же самое, что и EVT_CUSTOM, но задает диапазон значений идентификаторов окон. |
EVT_COMMAND(id, event, func) | То же самое, что и EVT_CUSTOM, но ожидает функцию-член с аргументом wxCommandEvent. |
EVT_COMMAND_RANGE(id1, id2, event, func) | То же самое, что и EVT_CUSTOM_RANGE, но ожидает функцию-член с аргументом wxCommandEvent. |
EVT_NOTIFY(event, id, func) | То же самое, что и EVT_CUSTOM, но ожидает функцию-член с аргументом wxNotifyEvent. |
EVT_NOTIFY_RANGE(event, id1, id2, func) | То же самое, что и EVT_CUSTOM_RANGE, но ожидает функцию-член с аргументом wxNotifyEvent. |
Список событий wxWidgets
Полный список классов событий доступен здесь: группы классов событий.
Комментариев нет:
Отправить комментарий