Выпуск 21. Ноябрь 2014
← Тестирование в Perl. Практика | Содержание | Еще немного об асинхронном программировании на Anyevent →GUI-приложения на Perl с помощью wxWidgets
Рассмотрены основы написания GUI-приложений с помощью wxWidgets
Написание GUI-приложений на Perl ничем не отличается от написания на других динамических языках. Все зависит от качества оберток над оригинальными библиотеками и их своевременного обновления.
Почему wxWidgets
?
wxWidgets
является по-настоящему кросс-платформенным GUI-инструментарием. В отличие от, например, Qt
, который тоже вроде бы является кросс-платформенным, wxWidget на запускаемых платформах использует родные библиотеки, а не пытается их эмулировать. Таким образом приложения выглядят не cовсем одинаково, но совершенно привычно для своей графической оболочки.
Более того, поддержка у wxWidgets
для Perl является полной, качественной и современной. Например, популярный редактор Padre
тоже написан с помощью wxWidgets. Имея под рукой большой открытый и свободный проект, всегда можно подглядеть, как реализуются те или иные виджеты, можно на конкретном примере быстро научиться.
Настройка окружения
Для разработки я использую платформу GNU/Linux. Поэтому дальше приведена установка wxWidgets
именно для моей ОС. Проще всего устанавливать библиотеки из дистрибутивных пакетов, на примере Debian:
# apt-get install libwx-perl
Конечно, всегда можно установить и с CPAN, но тогда придется много компилировать и можно наткнуться на несовместимость версий.
$ cpanm Wx
Первое приложение
Стандартным первым приложением будет окно на котором написано “Hello, world!”:
use strict;
use warnings;
use Wx;
my $app = Wx::SimpleApp->new;
my $frame = Wx::Frame->new(undef, -1, "Hello, world!");
$frame->Show;
$app->MainLoop;
Wx::Frame
это окно, у которого может быть изменен размер, у него может быть статусная строка, а также меню. Т.е. это окно общего назначения. Кроме Wx::Frame
существует также Wx::Dialog
и другие. Для добавления визуальных элементов можно воспользоваться различными классами и методами wxWidgets
. Например, чтобы добавить кнопку, которая закрывает приложение, напишем следующий код:
use strict;
use warnings;
use Wx;
use Wx::Event;
my $app = Wx::SimpleApp->new;
my $frame = Wx::Frame->new(undef, -1, "Hello, world!");
my $button = Wx::Button->new($frame, -1, 'Close');
Wx::Event::EVT_BUTTON($frame, $button, sub { $frame->Close(1) });
$frame->Show;
$app->MainLoop;
Все методы, события и т.д. хорошо описаны в документации wxWidgets. Не будем на этом подробно останавливаться. Рассмотрим общий подход в написании GUI-приложений.
Генераторы XML-интерфейса
Для небольшого приложения это вполне допустимо, также как и для простого Perl-скрипта можно все записать в один файл. Однако это очень неудобно для больших приложений с большим количеством окон, диалогов, сложной логикой.
Для графических приложений применяют тот же подход, что и для веб-приложений — MVC, или разделение бизнес-логики, отображения и управления. Так же можно поступить и с GUI-программой.
Для построения интерфейса лучше воспользоваться специальными программами, генерирующими XML-настройки (XRC-файлы), которые, будучи загруженными в приложении, преобразуются в графический интерфейс. Это гораздо легче поддерживать и проще изменять.
Для wxWidgets
существует несколько таких генераторов. Наиболее полным является wxFormBuilder
. Он позволяет создавать всевозможные элементы, указывать их расположение на окнах и т.д.
Например, XRC-файл для предыдущего приложения может выглядеть так:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<resource xmlns="http://www.wxwindows.org/wxxrc" version="2.3.0.1">
<object class="wxFrame" name="MainFrame">
<style>wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL</style>
<size>500,300</size>
<title></title>
<centered>1</centered>
<aui_managed>0</aui_managed>
<object class="wxButton" name="CloseButton">
<label>Close</label>
<default>0</default>
</object>
</object>
</resource>
Теперь загрузим эту конфигурацию в приложении:
use strict;
use warnings;
use Wx;
use Wx::XRC;
use Wx::Event;
my $app = Wx::SimpleApp->new;
my $xrc = Wx::XmlResource->new();
$xrc->InitAllHandlers();
$xrc->Load('example.xrc');
my $frame = Wx::Frame->new;
$xrc->LoadFrame($frame, undef, 'MainFrame');
Wx::Event::EVT_BUTTON(
$frame,
Wx::XmlResource::GetXRCID('CloseButton'),
sub { $frame->Close(1) }
);
$frame->Show;
$app->MainLoop;
Для того, чтобы достать объект Wx::Frame
, используем метод LoadFrame
:
my $frame = Wx::Frame->new;
$xrc->LoadFrame($frame, undef, 'MainFrame');
А для того, чтобы достать объект кнопки, вначале получаем ее id
по имени:
Wx::XmlResource::GetXRCID('CloseButton')
А затем привязываем событие.
Всегда стоит понятным образом называть все элементы, а не оставлять что-то вроде m_Button_1
, это сделает управляющий код существенно понятнее.
Разделение на классы
Как и любое Perl-приложение, разделение на отдельные пакеты и классы упрощает понимание кода, делает возможным тестирование отдельных классов, а также позволяет легко наследовать и менять поведение графических элементов.
Разделим наше приложение на несколько составляющих.
Основной класс приложения
package MyApp;
use strict;
use warnings;
use base 'Wx::SimpleApp';
use Wx;
use Wx::XRC;
use MyMainFrame;
sub OnInit {
my $self = shift;
my $xrc = Wx::XmlResource->new();
$xrc->InitAllHandlers();
$xrc->Load('app.xrc');
my $frame = MyMainFrame->new;
$xrc->LoadFrame($frame, undef, 'MainFrame');
$frame->Show;
}
1;
Задача основого класса выполнить загрузку настроек, создать основное окно и показать его.
Класс основного окна
package MyMainFrame;
use strict;
use warnings;
use base 'Wx::Frame';
use Wx;
use Wx::Event;
use Wx::XRC;
sub new {
my $self = shift->SUPER::new(@_);
Wx::Event::EVT_BUTTON(
$self,
Wx::XmlResource::GetXRCID('CloseButton'),
sub { $self->Close(1) }
);
return $self;
}
1;
Задача этого класса выполнить настройку своего окна, т.е. прописать все события, в данном случае обработать нажатие на кнопку CloseButton
.
Скрипт запуска
Скрипт запуска app.pl
является входной точкой для запуска приложения.
#!/usr/bin/env perl
use strict;
use warnings;
use Wx;
use MyApp;
MyApp->new->MainLoop;
С помощью такой декомпозиции каждый класс занимается своим делом, а структура приложения выглядит следующим образом:
app.pl
app.xrc
lib/
MyApp.pm
MyMainFrame.pm
Можно у каждого класса сделать какой-то уникальный префикс, например GUI, чтобы их отличать от обычных Perl-классов, которые будут заниматься логикой приложения.
Генераторы Perl-кода
wxFormBuilder
, как было уже указано, может генерировать XML-представление, однако он позволяет гененировать и код. К сожалению, из коробки нет поддержки генерации Perl-кода. Однако, разработчики Padre реализовали собственный генератор Perl-кода из файла проекта wxFormBuilder. Для этого потребуются два модуля: FBP
и FBP::Perl
. Написав простейший скрипт для создания классов, получим на выходе дерево файлов, где каждый — это отдельное окно:
#!/usr/bin/env perl
use strict;
use warnings;
my ($project_file, $root_dir) = @ARGV;
die "Usage: <project_file> <dir>\n" unless $project_file && $root_dir;
die "'$project_file' does not exist\n" unless -e $project_file;
die "'$root_dir' does not exist\n" unless -d $root_dir;
use File::Slurp qw(write_file);
use File::Spec::Functions qw(catfile);
use File::Path qw(make_path);
use File::Basename qw(dirname);
use FBP;
use FBP::Perl;
my $fbp = FBP->new;
$fbp->parse_file($project_file);
my $project = $fbp->project;
my $generator = FBP::Perl->new(project => $project);
foreach my $form ($project->forms) {
my $content = $generator->flatten($generator->frame_class($form));
write_class($form->name, $content);
}
foreach my $dialog ($project->dialogs) {
my $content = $generator->flatten($generator->dialog_class($dialog));
write_class($dialog->name, $content);
}
sub write_class {
my ($class_name, $content) = @_;
my $path = join('/', split /::/, $class_name) . '.pm';
$path = catfile($root_dir, $path);
my $dir = dirname($path);
make_path($dir);
write_file $path, $content;
}
Плюс этого подхода еще и в том, что нет необходимости вручную привязывать сигналы к элементам. Указав в wxFormBuilder
название обработчика, PBP::Perl
правильно сгенерирует соответствующий код. Вот как, например, выглядит класс основной формы:
package MyApp::FBP::MainFrame;
use 5.008005;
use utf8;
use strict;
use warnings;
use Wx 0.98 ':everything';
our $VERSION = '0.01';
our @ISA = 'Wx::Frame';
sub new {
my $class = shift;
my $parent = shift;
my $self = $class->SUPER::new(
$parent,
-1,
'',
wxDefaultPosition,
[ 500, 300 ],
wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL,
);
$self->{CloseButton} = Wx::Button->new(
$self,
-1,
"Close",
wxDefaultPosition,
wxDefaultSize,
);
Wx::Event::EVT_BUTTON(
$self,
$self->{CloseButton},
sub {
shift->OnClick(@_);
},
);
my $bSizer1 = Wx::BoxSizer->new(wxVERTICAL);
$bSizer1->Add( $self->{CloseButton}, 0, wxALL, 5 );
$self->SetSizer($bSizer1);
$self->Layout;
return $self;
}
sub OnClick {
warn 'Handler method OnClick for event CloseButton.OnButtonClick not implemented';
}
1;
Как же использовать эти классы? Добавлять в них функционал не стоит, потому как они автоматически генерируются из файла проекта. Наиболее правильным решением будет унаследоваться от сгененированного класса и там уже имплементировать нужный код. Кроме того, имеет смысл генерировать классы с определенным префиксом, чтобы было понятно, какие классы автоматические, а какие написаны вручную.
Вот так выглядит реализация обработчика события OnClick
в дочернем классе:
package MyApp::MainFrame;
use strict;
use warnings;
use base 'MyApp::FBP::MainFrame';
sub OnClick {
my $self = shift;
$self->Close(1);
}
1;
Проект с использованием сгенерированных классов выглядит следующим образом:
app.pl
lib/
MyApp/
FBP/
MainFrame.pm
MainFrame.pm
MyApp.pm
generate.pl
simple.fbp
simple.fbp
это файл проекта wxFormBuilder, а generate.pl
— скрипт для генерации.
Модуль MyApp.pm
придется тоже написать вручную, но его код предельно прост:
package MyApp;
use strict;
use warnings;
use base 'Wx::SimpleApp';
use MyApp::MainFrame;
sub OnInit {
my $self = shift;
my $frame = MyApp::MainFrame->new;
$frame->Show;
}
1;
А app.pl
остается тем же, что и в случае использования XML-представления.
Разделение логики
Как уже упоминалось, при написании графических приложений разделяется собственно представление и логика самого приложения. Идеально, если приложение будет реализовано таким образом, что замена графической библиотеки другой, или же веб-приложением, или консольным вариантом не повлечет каких-либо или, как минимим, никаких серьезных изменений.
Например, если в приложении необходимо сделать запрос к веб-сайту для получения каких-то данных, стоит выделить это в отдельный класс, не зависящий от графической библиотеки. Это позволит гораздо проще его протестировать стандартными средствами. В случае тестирования графических программ это сделать гораздо сложнее.
Впрочем, тестировать графические интерфейсы тоже можно и нужно, как например, с помощью Selenium тестируют и веб-приложения. Для X11 есть набор инструментов Xnee, который позволяет записывать сценарии, а затем выполнять их в автоматическом режиме.
Блокировки
Если писать серьезные приложения на wxWidgets
, в скором времени вы столкнетесь с тем, что при выполнении требующих времени задач активное окно блокируется и как будто замирает. Все дело в блокировке. Во время выполнения вашего кода wxWidgets
ждет, пока он закончится.
Стандартным способом решения этой проблемы является использование тредов (потоков, нитей), когда задачи, которые необходимо выполнить на заднем фоне, выполняются в отдельном потоке. В этом случае окно не блокируется, а после завершения задачи основное окно получает ответ и обрабатывает его.
Кроме потоков возможно использование асинхронных фреймворков по типу POE
. На Linux возможно использование wxGTK
, а вместе с ним AnyEvent
. В этом случае придется писать событийно-ориентированный код со всеми преимуществами и недостатками последнего.
Также возможно выполнение фоновых задач в отдельном процессе с помощью wxExecute
, но это больше подходит для запуска сторонних приложений.
Выполнение задач в отдельном потоке
Самый распространенный и простой способ выполнить задачу в фоновом режиме — использовать потоки. Чтобы использовать потоки с Wx
, необходимо, во-первых, убедиться, что perl
собран с поддержкой threads
:
perl -V | grep threads
А во-вторых, подключить прагму threads
ДО подключения Wx
(это стоит сделать также в скрипте запуска приложения):
use threads;
use Wx;
Например, при нажатии на кнопку, мы хотим выполнить какой-то код и оповестить главное приложение о результате. Скелет такого приложения будет выглядеть следующим образом:
package MyApp::MainFrame;
use strict;
use warnings;
use threads;
use threads::shared;
use base 'MyApp::FBP::MainFrame';
my $DONE_EVENT : shared = Wx::NewEventType;
sub OnClick {
my $self = shift;
Wx::Event::EVT_COMMAND($self, -1, $DONE_EVENT, \&done);
threads->create(\&work, $self);
}
sub work {
my $handler = shift;
# do something
my @result : shared = ...;
my $threvent = Wx::PlThreadEvent->new(-1, $DONE_EVENT, \@result);
Wx::PostEvent($handler, $threvent);
threads->detach;
threads->exit;
}
sub done {
my $self = shift;
my ($event) = @_;
my $result = $event->GetData;
# do something
...
}
1;
Переменные с тегом shared
необходимы для обмена данными между потоками, так как потоки выполняются одновременно, и использование обычных переменных небезопасно. Wx
сам проследит, что переменные объявлены как shared
, обезопасив таким образом программиста.
В этом приложении при нажатии на кнопку регистрируется обработчик завершения события, далее создается поток для выполнения работы. Когда поток завершается, он сигнализирует главному приложению о своих результатах. Главное приложение в обработчике, зарегистрированном ранее, получает данные и производит какие-то действия для отображения результата.
Проектируя код таким образом, т.е. исполняя код в фоновом режиме, можно писать обычные Perl-модули совершенно независимо от графической библиотеки.
Пример приложения
Для примера, напишем приложение, которое получает от MetaCPAN десять последних загруженных модулей и отображает их в виде списка.
Модель
Вначале реализуем модель, т.е. основу приложения. В нашем случае это модуль, который будет получать последние добавленные дистрибутивы. Он может выглядеть следующим образом:
package MyApp::ModulesFetcher;
use strict;
use warnings;
use JSON ();
use LWP::UserAgent;
my $METACPAN = 'http://api.metacpan.org/v0';
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub fetch {
my $self = shift;
my $ua = LWP::UserAgent->new;
my $response = $ua->post(
"$METACPAN/release/_search",
Content => JSON::encode_json(
{
query => {
filtered => {
query => {"match_all" => {}},
filter => {
term => {'release.authorized' => \1},
}
}
},
fields => ['distribution'],
size => 10,
from => 0,
sort => [{date => 'desc'}],
}
)
);
return () unless $response->is_success;
my $res = JSON::decode_json($response->decoded_content);
my @distributions = @{$res->{hits}->{hits}};
return map { $_->{fields}->{distribution} } @distributions;
}
1;
Более подробно о том, как работать с API MetaCPAN, можно почитать в документации. Там же есть множество примеров.
Отображение
Графическое приложение будет представлять собой одно единственное окно с кнопкой Fetch
и списком. При нажатии на кнопку в отдельном потоке создаем объект класса ModulesFetcher
и возвращаем результат. Главное графическое приложение обрабатывает результат и заполняет список.
Скелет всего приложения уже описывался нами выше, поэтому здесь покажем только пример использования стороннего модуля:
package MyApp::MainFrame;
use strict;
use warnings;
use threads;
use threads::shared;
use base 'MyApp::FBP::MainFrame';
use MyApp::ModulesFetcher;
my $DONE_EVENT : shared = Wx::NewEventType;
sub OnClick {
my $self = shift;
Wx::Event::EVT_COMMAND($self, -1, $DONE_EVENT, \&done);
threads->create(\&work, $self);
}
sub work {
my $handler = shift;
my $fetcher = MyApp::ModulesFetcher->new;
my @modules = $fetcher->fetch;
my @result : shared = @modules;
my $threvent = Wx::PlThreadEvent->new(-1, $DONE_EVENT, \@result);
Wx::PostEvent($handler, $threvent);
threads->detach;
threads->exit;
}
sub done {
my $self = shift;
my ($event) = @_;
my $modules = $event->GetData;
my $list_ctrl = $self->{ModuleListCtrl};
$list_ctrl->ClearAll;
foreach my $module (reverse @$modules) {
$list_ctrl->InsertStringItem(0, $module);
}
}
1;
В методе work
вызывается метод fetch
на объекте модели, затем результат преобразуется в shared
-переменную, и создается сигнал главному окну с передачей результата.
В методе done
запрашиваем полученные данные у события с помощью метода GetData
и затем в цикле добавляем их в объект списка. Перед этим не забываем очистить старые данные.
Выводы
Писать графические приложения на Perl можно вполне успешно. С помощью библиотеки wxWidgets
это еще и просто. Правильно разделяя модель и отображение, можно добиться переносимости между различными графическими библиотеками, а также упростить реализацию.
Если читателям интересны подобные статьи, в планах есть написание аналогичного примера с помощью библиотеки Gtk
. Жду ваших комментариев.
← Тестирование в Perl. Практика | Содержание | Еще немного об асинхронном программировании на Anyevent →