Журнал LinuxFormat - перейти на главную

LXF99:wxWidgets

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

События и компоновка

ЧАСТЬ 2 Сегодня Андрей Боровский разберется с вопросами, без которых немыслимо создание любого приложения wxWidgets: расположением виджетов в окне, обработкой сообщений и интернационализацией.

Сегодня мы рассмотрим основные моменты программирования с wxWidgets: компоновку дочерних визуальных элементов, обработку событий и управление потоками на примере приложения, взаимодействующего с клиентом Skype. В результате получится программа Skype Monitor. Эта утилита воспроизводит функциональность одного из демо-приложений, написанных программистами Skype Limited на Qt. Наш вариант использует, естественно, wxWidgets. Программа позволяет посылать команды клиенту Skype, используя Skype Public API, и получать ответные сообщения. С помощью команд Skype Monitor можно даже записывать звонки Skype, хотя процесс и не автоматизирован. Базовая структура программы wxWidgets нам уже знакома, но вкратце напомню: мы создаем класс MyApp, производный от wxApp, и переопределяем в нем метод OnInit() класса-предка. Мы также создаем класс MainFrame, производный от wxFrame. Объект этого класса, создаваемый в методе MyApp::OnInit(), реализует главное окно программы.

Расположение дочерних элементов

Расположение дочерних визуальных элементов в wxWidgets контролируют специальные объекты – «sizers». По аналогии с Qt, я буду называть их менеджерами компоновки. Термин «контейнеры» был бы, наверное, уместнее, но контейнерами в wxWidgets именуются окна специальных типов (например, wxPanel). Менеджеры компоновки wxWidgets напоминают менеджеры компоновки Qt и контейнеры GTK+. От базового класса wxSizer происходит несколько специальных (wxBoxSizer, wxGridSizer, wxFlexGridSizer, wxStaticBoxSizer). Каждый из них реализует одну из простых схем расположения визуальных элементов. Например, wxBoxSizer может располагать дочерние элементы либо вертикально, в столбце, либо горизонтально – в линейке. Для конструирования более сложных интерфейсов создаются иерархии из менеджеров.

Наше приложение использует для построения интерфейса два объекта wxBoxSizer (переменные vbox и hbox). Объект vbox нужен нам для того, чтобы расположить в один ряд строку ввода команды и кнопку Send. Таким образом, создается единый блок «строка ввода + кнопка». Сам объект vbox становится дочерним элементом объекта hbox, который располагает свои дочерние элементы вертикально. Вторым дочерним элементом hbox становится окно просмотра результатов команды. Когда мы говорим о дочерних элементах объектов vbox и hbox, мы должны помнить, что класс wxSizer и его потомки не являются потомками wxWindow (в этом они похожи на менеджеры компоновки Qt). Каждый дочерний элемент объектов hbox и vbox является также и дочерним элементом главного окна.

Давайте посмотрим, как все это выглядит в коде. Ниже приводится фрагмент из конструктора класса MainForm:

vbox = new wxBoxSizer(wxVERTICAL);
hbox = new wxBoxSizer(wxHORIZONTAL);
commandEdit = new wxTextCtrl(this, ID_ENTER, "PROTOCOL 6",
wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
hbox->Add(commandEdit, 1, wxALL, 2);
commandButton = new wxButton(this, ID_START, "Send");
hbox->Add(commandButton, 0, wxALIGN_LEFT|wxALL, 2);
vbox->Add(hbox, 0, wxEXPAND);
logViewer = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY|wxTE_RICH);
vbox->Add(logViewer, 1, wxEXPAND|wxALIGN_CENTER|wxALL, 2);
SetSizer(vbox);
vbox->Fit(this);

Создавая объект vbox, мы передаем в параметре конструктора константу wxVERTICAL, которая указывает, что новый менеджер компоновки должен располагать дочерние элементы вертикально. Аналогично создается объект hbox, только на этот раз мы задаем горизонтальное расположение дочерних элементов. Объект commandEdit класса wxTextCtrl реализует строку ввода команды. После того, как мы создали этот объект, мы добавляем его в менеджер компоновки hbox с помощью метода Add(). Точно так же мы добавляем в менеджер компоновки объект commandButton, представляющий кнопку. Обратите внимание на списки аргументов метода Add() в первом и во втором случаях. Первый параметр, естественно, указатель на добавляемый объект. Далее следует аргумент, который можно уподобить параметру stretch factor менеджеров компоновки Qt. Он определяет, должны ли размеры визуального элемента меняться вместе с размерами окна. Следующий аргумент представляет собой комбинацию флагов, управляющих изменением размера добавляемого элемента, его выравниванием и применением отступов. Константа wxEXPAND указывает на то, что добавляемый элемент должен заполнять все доступное свободное пространство менеджера компоновки. Константа wxLEFT выравнивает соответствующий визуальный элемент по левому краю. Константа wxALL определяет, что отступ (ширина которого указывается в следующем параметре) должен быть применен по всем краям дочернего элемента. Мы делаем менеджер компоновки hbox дочерним элементом vbox, в результате чего vbox управляет строкой ввода команд и кнопкой как единым элементом. Вторым дочерним элементом vbox становится объект logViewer (окно просмотра результатов команд).

Вернемся к изменению размеров дочерних элементов. Фактически, этим процессом управляют значения второго и третьего параметров метода Add(). Второй параметр указывает, должны ли размеры дочернего элемента меняться в «главном» направлении менеджера компоновки wxBoxSizer (то есть, для «вертикальных» менеджеров – по высоте, а для «горизонтальных» – по ширине). Что касается изменения размеров элемента в другом направлении (например, при изменении ширины «вертикального» менеджера), то на него влияет наличие константы wxEXPAND в третьем параметре, а также свойства самого дочернего элемента. Мы подбираем значения этих параметров, исходя из соображений здравого смысла. Кнопка должна иметь фиксированную ширину и высоту, поэтому во втором параметре Add() передается значение 0, а в третьем отсутствует константа wxEXPAND. Строка ввода должна быть фиксирована по высоте, но ее ширина может меняться при изменении ширины окна. Соответственно, во втором параметре Add() передается значение 1. Мы нигде не указываем специально, что высота строки ввода и кнопки должна быть постоянна, но этого и не требуется, так как высота этих элементов фиксирована по умолчанию. Для того, чтобы высота объекта hbox не менялась, мы передаем значение 0 во втором параметре вызова Add(), который добавляет этот объект в объект vbox. Окно logViewer, которое представляет собой объект того же класса wxTextCtrl, но только в многострочном варианте, должно иметь переменную высоту и ширину, поэтому во втором параметре метода Add() передается значение 1, а в третий параметр добавляется константа wxEXPAND.

Нам остается объяснить главному окну, что управление его дочерними элементами доверено объекту vbox и его дочерним элементам. Для этого мы вызываем метод SetSizer() класса wxFrame, имя которого говорит само за себя. Далее мы вызываем метод Fit() нашего «главного» менеджера компоновки vbox и передаем ему указатель на объект главного окна. Метод Fit() подстраивает размеры окна под размеры контейнера (а не наоборот, как можно было бы ожидать).

События

Как уже отмечалось, wxWidgets поддерживает два механизма обработки событий. Более старый механизм, использующий статические таблицы обработчиков событий, был навеян библиотекой MFC. Новый вариант, основанный на классах-обработчиках, позволяет назначать обработчики динамически, во время выполнения программы. Этот механизм напоминает сигналы и слоты, используемые в Qt. В книге «Cross-Platform  GUI  Programming  with  wxWidgets» (напоминаю адрес: http://phptr.com/perens) внимание читателя (и авторов) сосредоточено, в основном, на обработке событий в стиле MFC. В противоположность этому, мы углубимся в обработку событий «в стиле Qt» и лишь кратко рассмотрим альтернативный вариант.

Система обработки событий wxWidgets основана на классе wxEvtHandler. Этот класс используется и при обработке событий, обработчики которых заданы в таблицах, и при обработке событий «в стиле Qt». Для динамического назначения и удаления обработчика служат методы Connect() и Disconnect() класса wxEvtHandler. Класс wxEvtHandler является одним из предков базового для всех окон класса wxWindow, так что внутри методов потомков wxWindow методы Connect() и Disconnect() доступны всегда. В конструкторе класса MainFrame мы, помимо прочего, назначаем обработчик событию wxEVT_COMMAND_BUTTON_CLICKED кнопки commandButton. Для этого используется следующий вызов Connect():

 Connect(ID_START, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(MainFrame::OnCommand));

Первый параметр метода Connect() – это идентификатор кнопки, который, как мы видели выше, был передан ей в конструкторе. В Qt и GTK+ в этом месте можно было бы ожидать использование адреса объекта-кнопки. Практика использования назначенных объектам числовых идентификаторов заимствована в wxWidgets из MFC. Библиотека предоставляет нам ряд идентификаторов по умолчанию. Например, если бы наше окно содержало кнопки OK и Cancel, мы могли бы использовать для них стандартные идентификаторы wxID_OK и wxID_CANCEL, соответственно. Конечно, ничто не мешает нам использовать эти идентификаторы и для других типов кнопок (если их не приходится использовать по назначению), но для того, чтобы код приложения был более понятным, для нестандартных элементов управления лучше использовать нестандартные идентификаторы. Порядок определения собственных идентификаторов элементов управления так же позаимствован из MFC. В wxWidgets определена константа wxID_HIGHEST, которая содержит максимальное значение, задействованное библиотекой в качестве идентификатора. Мы можем использовать для собственных идентификаторов любые значения, превышающие wxID_HIGHEST, не опасаясь, что они совпадут с уже определенными идентификаторами. Например, константа ID_START определена нами как

 #define ID_START wxID_HIGHEST + 1

Если нам понадобятся еще идентификаторы, мы воспользуемся значениями wxID_HIGHEST + 2 и т.д.

Вторым аргументом метода Connect() является имя события. В третьем параметре методу передается указатель на обработчик события. В объявлении метода Connect() третий параметр имеет тип wxObjectEventFunction. Указатель на фактически используемый метод-обработчик должен быть приведен к этому типу с помощью специального макроса. В нашем примере обработку события выполняет метод MainFrame::OnCommand(), а необходимое преобразование типа выполняется с помощью макроса wxCommandEventHandler(). Этот макрос используется для всех событий, чьи имена имеют префикс wxEVT_COMMAND (события, которые генерируются различными элементами управления пользовательского интерфейса). У метода Connect() есть и другие параметры, но их значения редко задаются явным образом.

Определение метода-обработчика OnCommand выглядит так:

 void MainFrame::OnCommand(wxCommandEvent& event)

Обработчик не возвращает значений, а единственным его аргументом является ссылка на объект класса wxCommandEvent. Один и тот же обработчик может использоваться для обработки самых разных событий от разных элементов управления. В этом случае из параметра event мы можем получить дополнительную информацию о событии. Класс wxCommandEvent является потомком класса wxEvent – базового для аргументов всех обработчиков событий.

Для генерации событий класс wxEvtHandler и его потомки предоставляют нам методы ProcessEvent() и AddPendingEvent(). Первый метод непосредственно вызывает обработчик события, второй метод позволяет поместить событие в очередь на обработку. У обоих методов определен только один параметр, в котором передается ссылка на объект класса-потомка класса wxEvent. Основываясь на типе этого объекта, wxWidgets определяет, обработчик какого события следует вызвать. Если с методом ProcessEvent() вам, скорее всего, не придется иметь дела, если только вы не пишете собственный визуальный элемент для wxWidgets, то метод AddPendingEvent() используется довольно часто, и мы к нему еще вернемся.

Итак, подведем итог. При создании каждого визуального элемента, который может быть источником событий, мы присваиваем ему числовой идентификатор, заданный константой (напомню, что мы можем выбрать значение wxID_ANY, если нам не нужно обрабатывать события данного элемента). Чтобы назначить обработчик события, мы вызываем метод Connect() объекта, реализующего главное окно. Этому методу передаются идентификатор визуального элемента, имя события и обработчик события. Однако не со всеми компонентами wxWidgets дело обстоит так же просто. Рассмотрим, например, класс wxSocketServer, который инкапсулирует сокет, открытый для приема входящих соединений. Каждый раз, когда состояние сокета меняется (например, поступает запрос на соединение), объект этого класса генерирует событие wxEVT_SOCKET. Мы могли бы ожидать, что назначить обработчик этого события так же легко, как и для события wxEVT_COMMAND_BUTTON_CLICKED объекта-кнопки, но это не так. Прежде всего, в конструкторе wxSocketServer нет параметра, в котором можно было передать идентификатор объекта (как это делается при конструировании кнопки), а без идентификатора связать объект с обработчиком события нельзя. Почему в конструкторе wxSocketServer нельзя назначить объекту идентификатор? Идентификаторы обрабатываются объектами, приводимыми к типу wxEvtHandler, например, потомками класса wxWindow. Класс wxSocketServer не является потомком wxEvtHandler, поэтому своего средства обработки сигналов у него нет. Это логично, если вспомнить, что в основе обработки событий лежит механизм обработки сообщений оконной системы, а объект wxSocketServer сам по себе не связан ни с каким окном. Для того, чтобы наладить обработку событий wxSocketServer, нам придется «одолжить» объект, приводимый к wxEvtHandler у какого-либо окна. Сделать это можно, например, так:

sockServ = new wxSocketServer(ip);
sockServ->SetEventHandler(*this, ID_SOCKET);
sockServ->SetNotify(wxSOCKET_CONNECTION_FLAG);
sockServ->Notify(TRUE);
Connect(ID_SOCKET, wxEVT_SOCKET, wxSocketEventHandler(MainFrame::OnConnection));

Приведенный фрагмент позаимствован из конструктора некоего класса-потомка wxFrame. Переменная sockServ – это указатель на объект wxSocketServer. В конструкторе wxSocketServer передается переменная типа wxSockAddress, которая нас сейчас не интересует. Мы связываем объект wxSocketServer с объектом wxEvtHandler с помощью метода SetEventHandler(). Первым параметром метода должна быть ссылка на объект класса wxEvtHandler (или класса, приводимого к нему). Поскольку этот код вызывается из конструктора главного окна, а класс окна является потомком wxEvtHandler, мы можем передать в этом параметре значение *this. Вторым аргументом метода SetEventHandler() должен быть идентификатор объекта, который используется для назначения обработчика событий (таким образом мы связываем объект sockServ с идентификатором). Константа ID_SOCKET определена нами так же, как и константа ID_START в рассмотренном выше примере. Метод SetNotify() позволяет нам указать, какие именно внутренние события сокета должны приводить к генерации события wxEVT_SOCKET (благодаря этому методу мы можем отсеять неинтересные для нас события еще до вызова обработчика wxEVT_SOCKET). Наконец, метод Notify(), вызванный с параметром TRUE, включает передачу событий объектом-сокетом. Теперь объект sockServ готов к связыванию с обработчиком событий, которое мы выполняем с помощью метода Connect() того же объекта wxEvtHandler, с которым мы связали сокет (то есть объекта this).

Посмотрим теперь, что требуется для генерации события. Допустим, мы хотим сгенерировать событие wxCommandEvent из метода некоторого класса-потомка wxWindow. Прежде всего, нам следует создать объект класса wxCommandEvent:

 wxCommandEvent event(EVENT_TYPE, CONTROL_ID);

Константа EVENT_TYPE обозначает здесь конкретный тип события wxCommandEvent, а константа CONTROL_ID – это тот самый идентификатор объекта-источника события, о котором мы много говорили выше. Для передачи события мы вызываем метод AddPendingEvent():

AddPendingEvent(event);

Вызов AddPendingEvent() не приводит к немедленному выполнению обработчика события. Он является асинхронным и просто ставит событие в очередь на обработку (поэтому AddPendingEvent() очень удобно использовать тогда, когда нам требуется передать событие главному потоку программы из вспомогательного потока). Когда очередь дойдет до обработки нашего события, соответствующий обработчик получит копию объекта event в качестве аргумента.

Рассмотрим этот механизм внимательней. Мы передаем ссылку на объект события методу AddPendingEvent(), после чего управление событием переходит к wxWidgets. Мы не знаем, когда именно будет вызван обработчик события, и не можем контролировать время жизни объекта-события. Дабы избавить нас от хлопот, метод AddPendingEvent() копирует переданный ему объект-событие и использует далее эту копию, а созданный нами объект остается под нашим контролем. У этого факта есть два следствия. Во-первых, у всех классов-потомков wxEvent должен быть реализован механизм «глубокого» копирования (вы должны помнить об этом, когда будете создавать собственные классы объектов-событий), а во-вторых, методу AddPendingEvent() можно передавать ссылку на объект, созданный в стеке вызывающей функции (что мы и сделали в приведенном выше примере).

Теперь скажем несколько слов о статической обработке событий. Для того, чтобы сделать все «в стиле MFC», мы должны объявить и реализовать таблицы событий. Такая таблица может быть объявлена только в классе-потомке класса wxEvtHandler. Чтобы объявить в классе таблицу событий, достаточно добавить в объявление класса макрос DECLARE_EVENT_TABLE(). Например, если у нас есть класс MyFrame, наследующий классу wxFrame, объявление таблицы событий может выглядеть так:

class MyFrame : public wxFrame
{
public:
void OnButtonClick(wxCommandEvent& event);
private:
DECLARE_EVENT_TABLE()
wxButton * m_button;
};

Метод OnButtonClick() должен стать обработчиком события EVT_BUTTON кнопки m_button. В файл реализации класса следует добавить макросы

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_BUTTON(ID_BUTTON, MyFrame::OnButtonClick)
END_EVENT_TABLE()

Как видите, описание таблицы событий выглядит довольно просто. Между макросами BEGIN_EVENT_TABLE() и END_EVENT_TABLE() мы добавляем по одному макросу для каждого обрабатываемого события. Имена макросов соответствуют типам событий, первым аргументом макроса должен быть идентификатор объекта-источника события, вторым – метод-обработчик. Недостатком данного подхода, помимо его громоздкости, является и то, что связывание идентификаторов событий с обработчиками выполняется на этапе компиляции. Кроме того, статические таблицы не могут редактироваться во время выполнения программы (на то они, собственно, и статические).

Интернационализация

При интернационализации приложений wxWidgets применяются те же инструменты, что и для других приложений Linux – пакет gettext и его друзья, наделенные графическим интерфейсом. Подготовка самих приложений к интернационализации выполняется практически так же, как и при использовании других библиотек. Строки, предназначенные для перевода, выделяются макросом _() или функцией wxGetTranslation(). Любопытно, что строки, не предназначенные для перевода, тоже следует выделять макросами wxT() или _T(), если используется Unicode. Загрузкой ресурсов интернационализации управляет класс wxLocale. Объект класса wxLocale создается обычно в методе OnInit() объекта потомка wxApp. Далее вызываются, как минимум, два метода объекта wxLocale. Метод Init() включает поддержку выбранной локали и загружает каталог сообщений wxWidgets, используемый библиотекой по умолчанию. Метод AddCatalog() используется для загрузки каталога сообщений данного приложения.

В заключение этого краткого обзора wxWidgets я хотел бы отметить одну важную, хотя и не очень заметную особенность этого набора виджетов. Код, который создается на wxWidgets, обычно изолирован от низкоуровневых элементов системы значительно сильнее, нежели код, использующий Qt или GTK+. По сравнению с Qt и GTK, в wxWidgets совсем не так просто организовать обработку сообщений X11 или получить, например, дескриптор сокета. С одной стороны это хорошо, так как у программистов не возникает соблазна задействовать специфические для данной системы функции и сделать код приложения менее переносимым. С другой стороны, при попытке расширить возможности wxWidgets за счет подключения новых интерфейсов недоступность низкоуровневых функций может вызвать серьезные трудности.

В общем и целом можно сказать, что если для написания «родных» программ Linux лучше подходят Qt/KDE и GTK+, то библиотека wxWidgets может стать подходящим выбором для кроссплатформенных приложений, особенно для таких, которые сначала разрабатываются на платформе Windows, а уже потом переносятся на другиесистемы. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика