Vala и Gtk.Builder
Aug. 21st, 2008 09:59 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
С подачи
bugware я играюсь с Vala и построением интерфейсов на GTK+. Vala -- это такой продвинутый макротранслятор с C#-подобного по синтаксису языка на C с активным применением GObject. Все, что не очень хорошо читалось в коде на C при программировании GTK+, преображается в Vala.
Мы строим интерфейс программы в Glade, конвертируем его с помощью gtk-builder-convert в формат, который понимает класс Gtk.Builder из GTK+ 2.14 и старше, а затем динамически загружаем этот интерфейс в программу и используем его.
Сам по себе Vala -- полноценный язык, на нем можно просто писать приложения и без использования GTK+. В данном случае мне было интересно, а как сделать так, чтобы интерфейс рисовался в Glade, грузился во время исполнения, а обработчики сигналов к элементам интерфейса задавались бы в виде лямбда-функций, которые поддерживаются в Vala.
Естественно, можно автоматически связывать обработчики сигналов с загруженным интерфейсом через Gtk.Builder.connect_signals(), который просто берет имена обработчиков из описания интерфейса и ищет эти символы в загруженном приложении. Но хотелось разобраться с лямбдами.
Код приложения на Vala, в котором у нас в интерфейсе только две работающие команды: File->Open вызывает диалог выбора файла (и показ имени выбранного файла), а File->Quit выполняет выход из программы, выглядит так:
В App.connect_my_signals() мы используем switch для расстановки наших обработчиков сигналов. В Gtk.Builder предполагается, что мы по имени обработчика, прописанного в интерфейсе, можем определить какую функцию в нашем коде регистрировать как сигнал (если используем штатный Gtk.Builder.connect_signals(), который делает это автоматически).
В нашем случае мы хотим эти сигналы связать вручную (у лямбда-функций нет заранее известных имен). Для повышения читаемости исходного кода на Vala мы используем switch по имени сигнала и по имени виджета, к которому относится сигнал. Имена виджетов, как и имена обработчиков сигналов, мы задаем в Glade (или руками в файле описания интерфейса Gtk.Builder) заранее, поэтому можем сами себе гарантировать их уникальность. Поскольку по этим именам виджетов Gtk.Builder делает поиск ссылок между объектами при загрузке описания интерфейса, то имена попадают в специальный кэш, используемый функциями g_quark_from_string()/g_quark_from_string_static(). Туда же попадают и имена сигналов, которые регистрируют классы GObject при создании их в системе.
То есть, к моменту вызова App.connect_my_signals() у нас в кэше кварков уже есть все нужные строки и сравнение по ним будет довольно эффективным. Чего не скажешь об именах обработчиков сигналов, описанных в файле интерфейса: эти имена используем только мы и только в момент назначения сигналов.
В результате (можно посмотреть на сгенерированный Vala код на C), получается довольно эффективный код, который в своем первоначальном виде на Vala к тому же хорошо читаем человеком. А это, пожалуй, главное. Для автоматической регистрации пришлось бы только вместо connect_signals_full() вызывать connect_signals(self), но тогда бы лямбды не использовались. Реализацию оставлю вам. :-)
Код примера вместе с описанием интерфейса можно скачать здесь: http://boids.name/extract/myapp.tar.bz2
Собирается он так: valac -g --pkg glib-2.0 --pkg gtk+-2.0 myapp.vala, запускать надо myapp.
P.S. В Сизиф я Vala скоро соберу, текущая работа идет тут: http://git.altlinux.org/people/ab/packages/vala.git. Есть несколько нюансов, связанных с тем, что я хочу аккуратно запаковать все расширения, чтобы у них были правильные зависимости. Но это будет скоро сделано.
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
Мы строим интерфейс программы в Glade, конвертируем его с помощью gtk-builder-convert в формат, который понимает класс Gtk.Builder из GTK+ 2.14 и старше, а затем динамически загружаем этот интерфейс в программу и используем его.
Сам по себе Vala -- полноценный язык, на нем можно просто писать приложения и без использования GTK+. В данном случае мне было интересно, а как сделать так, чтобы интерфейс рисовался в Glade, грузился во время исполнения, а обработчики сигналов к элементам интерфейса задавались бы в виде лямбда-функций, которые поддерживаются в Vala.
Естественно, можно автоматически связывать обработчики сигналов с загруженным интерфейсом через Gtk.Builder.connect_signals(), который просто берет имена обработчиков из описания интерфейса и ищет эти символы в загруженном приложении. Но хотелось разобраться с лямбдами.
Код приложения на Vala, в котором у нас в интерфейсе только две работающие команды: File->Open вызывает диалог выбора файла (и показ имени выбранного файла), а File->Quit выполняет выход из программы, выглядит так:
using GLib; using Gtk; public class App : Gtk.Builder { private Gtk.Window _main_window; public Gtk.Window window { get { return _main_window; }} public signal void open_file(string file_name); public void exit_with_message_on_error(string error_message) { Gtk.MessageDialog message = new Gtk.MessageDialog(null, DialogFlags.MODAL, MessageType.ERROR, ButtonsType.OK, error_message); message.run(); message.destroy(); Gtk.main_quit(); /* never reached */ } public void connect_my_signals(Gtk.Builder builder, GLib.Object object, string signal_name, string handler_name, GLib.Object connect_object, GLib.ConnectFlags flags) { Gtk.Action action = object as Gtk.Action; if (null == action) { exit_with_message_on_error("Expected Gtk.Action!\n"); return; } switch (signal_name) { case "activate": switch (action.name) { case "File|Menu|Open": /* Define signal handler as lambda function */ action.activate += (app) => { Gtk.FileChooserDialog fchooser = new Gtk.FileChooserDialog("Open a file", (app as App).window, FileChooserAction.OPEN, STOCK_CANCEL, ResponseType.CANCEL, STOCK_OPEN, ResponseType.ACCEPT); int result = fchooser.run(); fchooser.hide(); if (result == ResponseType.ACCEPT) { open_file(fchooser.get_filename()); } fchooser.destroy(); }; break; /* This is File|Quit in our sample UI */ case "File|Menu|Quit": action.activate += Gtk.main_quit; break; default: break; } break; default: break; } } public void process_files() { try { add_from_file("myapp.gtk"); } catch (GLib.Error er) { exit_with_message_on_error(er.message); return; } _main_window = get_object("main_window") as Gtk.Window; connect_signals_full(connect_my_signals); _main_window.destroy += Gtk.main_quit; _main_window.realize(); _main_window.show_all(); /* Process file opening after selection as lambda function */ open_file += (app, file_name) => { Gtk.MessageDialog message = new Gtk.MessageDialog(app.window, DialogFlags.MODAL, MessageType.INFO, ButtonsType.OK, file_name); message.run(); message.destroy(); }; Gtk.main(); } static int main (string[] args){ Gtk.init(ref args); App app = new App(); app.process_files(); return 0; } }
В App.connect_my_signals() мы используем switch для расстановки наших обработчиков сигналов. В Gtk.Builder предполагается, что мы по имени обработчика, прописанного в интерфейсе, можем определить какую функцию в нашем коде регистрировать как сигнал (если используем штатный Gtk.Builder.connect_signals(), который делает это автоматически).
В нашем случае мы хотим эти сигналы связать вручную (у лямбда-функций нет заранее известных имен). Для повышения читаемости исходного кода на Vala мы используем switch по имени сигнала и по имени виджета, к которому относится сигнал. Имена виджетов, как и имена обработчиков сигналов, мы задаем в Glade (или руками в файле описания интерфейса Gtk.Builder) заранее, поэтому можем сами себе гарантировать их уникальность. Поскольку по этим именам виджетов Gtk.Builder делает поиск ссылок между объектами при загрузке описания интерфейса, то имена попадают в специальный кэш, используемый функциями g_quark_from_string()/g_quark_from_string_static(). Туда же попадают и имена сигналов, которые регистрируют классы GObject при создании их в системе.
То есть, к моменту вызова App.connect_my_signals() у нас в кэше кварков уже есть все нужные строки и сравнение по ним будет довольно эффективным. Чего не скажешь об именах обработчиков сигналов, описанных в файле интерфейса: эти имена используем только мы и только в момент назначения сигналов.
В результате (можно посмотреть на сгенерированный Vala код на C), получается довольно эффективный код, который в своем первоначальном виде на Vala к тому же хорошо читаем человеком. А это, пожалуй, главное. Для автоматической регистрации пришлось бы только вместо connect_signals_full() вызывать connect_signals(self), но тогда бы лямбды не использовались. Реализацию оставлю вам. :-)
Код примера вместе с описанием интерфейса можно скачать здесь: http://boids.name/extract/myapp.tar.bz2
Собирается он так: valac -g --pkg glib-2.0 --pkg gtk+-2.0 myapp.vala, запускать надо myapp.
P.S. В Сизиф я Vala скоро соберу, текущая работа идет тут: http://git.altlinux.org/people/ab/packages/vala.git. Есть несколько нюансов, связанных с тем, что я хочу аккуратно запаковать все расширения, чтобы у них были правильные зависимости. Но это будет скоро сделано.