Vala и Gtk.Builder
Aug. 21st, 2008 09:59 pmС подачи
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. Есть несколько нюансов, связанных с тем, что я хочу аккуратно запаковать все расширения, чтобы у них были правильные зависимости. Но это будет скоро сделано.
Мы строим интерфейс программы в 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. Есть несколько нюансов, связанных с тем, что я хочу аккуратно запаковать все расширения, чтобы у них были правильные зависимости. Но это будет скоро сделано.