Приложения с графическим интерфейсом в Perl

Доброе утро!

Бывают ситуации, когда нужно или просто хочется сделать приложение с графическим интерфейсом, но отказываться от привычных технологий не хочется. В этой статье я приведу пример небольшого приложения на perl с использованием Tk.

Tk — довольно простой в использовании набор инструментов, позволяющий строить графические интерфейсы разной степени сложности и навороченности. У Tk есть ряд преимуществ, в частности — кроссплатформенность, привязки ко множеству языков, настраиваемость. Также, для Tk можно создавать собственные компоненты.

В этой статье мы попробуем построить простое приложение, чтобы ознакомиться с базовыми возможностями Tk и тем, как пользоваться ими из Perl. Приложение будет отображать дерево процессов/потоков на текущей Linux-системе, а также выводить информацию о процессе и уметь посылать им сигналы. Полный его код можно найти здесь: https://github.com/ValeriyKr/procwatch.

Из внешнех зависимостей понадобится сам Tk, на CPAN могуль так и называется. Пользователям тайловых оконных менеджеров стоит отметить, что при установке с CPAN запускаются тесты. Тесты рисуют всякие разные окошки, которые в тайловых WM могут отображаться не совсем так, как ожидают тесты. Результат очевиден, как бороться — надеюсь, понятно.

Теперь к коду. Для того, чтобы просто сделать окошко, достаточно всего нескольких строк:

use Tk;
use strict;
use warnings;

my $mw = MainWindow->new;
$mw->configure(
  -width  => 640,
  -height => 480,
  -title  => 'ProcWatch',
);

MainLoop;

Код очевиден — создаём инстанс виджета MainWindow, настриваем его, запускаем основной цикл приложения. При настройке задаются размеры окна и заголовок. Все возможные настройки описываются документацией на виджеты и их родительские классы, так что приводить их смысла не имеет.

Поместим в наше окно объект, который будет способен отображать древовидные списки:

my $proctree = $mw->Scrolled('Tree',
  -scrollbars => 'se',
  -separator  => '/',
  -command    => sub { show_process_menu $_[0] =~ s{.*/}{}r },
);
$mw->bind('<Configure>' => sub {
  my ($width, $height) = $mw->geometry =~ m{([0-9]+)x([0-9]+)};
  $proctree->place(-width => $width, -height => $height);
});

Интересных моментов здесь два. Первый — это -command, задающий обработчик события двойного клика. В данном случае это анонимная функция, преобразующая путь (об этом дальше) в представление, пригодное определённой в другом месте программы функции show_process_menu. Второй — это вызов bind. В данном конкретном случае он позволяет среагировать на изменение настроек главного окна, подогнав размеры дерева под него.

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

sub refresh {
  # ...
  # Код для обновления дерева
  # ...
  $proctree->after(1000, \&refresh);
}
$proctree->after(0, \&refresh);

Метод after у компонента $proctree сразу после создания (через 0 миллисекунд) вызовет функцию refresh, так как на неё была передана ссылка. Та сделает какие-то действия по актуализации списка процессов и снова повесит такой же таймер, только уже с интервалом в секунду, чтобы процессы не мельками, да и чтобы поцессор не грузить.

Сам код формирования списка процессов приводить не буду, так как это уже не про Tk, но его можно увидеть по ссылке, данной выше. Что более интересно, так это то, как заполняется дерево.

Дерево, созданное нами, являеся, по сути, подобием структуры «префиксное дерево» (prefix tree, trie). Опция -separator => '/', которую мы задали при конструировании говорит дереву о том, что является разделителем на пути к значению. Соответственно, если мы напишем подобный код:

$proctree->add('1',     'a');
$proctree->add('1/1',   'b');
$proctree->add('1/1/1', 'C');
$proctree->add('1/1/2', 'D');
$proctree->add('1/4',   'e');
$proctree->add('1/4/1', 'F');

То компоненты с текстом C и D отобразятся на одном уровне и с одним предком, а F будет иметь с ними предка только в корне. Я же у себя выстраиваю дерево, основываясь на иерархии процессов (родительский-дочерний) и потоков этих процессов. Корневой элемент — 1, init. А в упомянутый ранее коллбэк приходит путь к элементу, который и редактируется, как это было замечено когда-то выше. Обработчик двойного клика запускает меню процесса, по которому кликнули.

Функция имеет следующий код:

sub show_process_menu($) {
  my ($proc, $prev) = ($_[0], '%0');
  my $pw = $mw->Toplevel(-title => "ProcWatch [$proc]");
  $prev = $pw->Label(-text => "Control $proc:")->form(
    -left => '%0', -right => '%100', -top => $prev,);
  $prev = $pw->Button(
    -command => sub { show_signal_menu $pw, $proc },
    -text    => 'Signalize',
  )->form(-left => '%0', -right => '%100', -top => $prev,);
  $prev = $pw->Button(
    -command => sub {
      open my $stfd, '<', "/proc/$proc/status" or return;
      my $st = join '', <$stfd>;
      close $stfd;
      my $statusfile = $pw->Toplevel(-title => "ProcWatch [$proc]: status");
      my $statustext = $statusfile->Scrolled('Text', -scrollbars => 'se');
      $statustext->Insert($st);
      $statustext->configure(-state => 'disabled');
      $statustext->pack;
    },
    -text    => 'Show status file',
  )->form(-left => '%0', -right => '%100', -top => $prev);
}

Здесь мы можем увидеть создание нового окошка (Toplevel), на котором есть текстовая надпись (Label) и пара кнопок. Также можно обратить внимание на вызов form(). Он позволяет разместить эелементы так, чтобы по ширине они заняли всё окно, а по высоте выстроились по порядку друг за другом, что достигается указанием ссылки на предыдущий созданный объект в качестве верхнего края элемента (-top => $prev).

Здесь же мы видим, как одна из кнопок при нажатии на неё вызывает некую функцию, а вот другая рисует новое окошко прямо в обработчике этого нажатия. Это тоже окно с кнопкой, но теперь мы видим, что в него снова добавлен элемент Scrolled. В этот раз это не дерево (Tree), а многострочное текстовое поле. Вызовы его методов и их аргументы говорят за себя. Пояснить стоит разве что назначение pack. Это метод, требующий использования менеджера компановки, который развезёт поле по всему окну.

Окошко же, вызываемое первой кнопкой построено по принципу того же, что и внешнее, с кнопками, так что рассматривать его не интересно.

Таким образом, мы реализовали приложение с простым графическим интерфейсом, навыки построения которого легко перенести в другую систему и даже в другой язык программирования, потому что хорошую поддержку Tk имеет много ЯП.

Готовые к запуску исходники можно скачать здесь, а адаптировать их подо что-то другое совершенно не сложно. В этом и прелесть Tk.