Speaker Rabbit

abbra


CIFS: curious information funneled sometimes


Previous Entry Share Next Entry
Vala и Gtk.Builder
Speaker Rabbit
abbra
С подачи 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 выполняет выход из программы, выглядит так:
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. Есть несколько нюансов, связанных с тем, что я хочу аккуратно запаковать все расширения, чтобы у них были правильные зависимости. Но это будет скоро сделано.
Tags: , , ,

  • 1
Я надеюсь, события и прочие кнопки создаются визуальным редкатором?

Это же обычный Glade, просто рисуешь все, что надо в интерфейсе. :-) А файл описания интерфейса потом конвертируешь в формат, который понимает штатный gtk+ (без всяких дополнительных libgladeui) и грузишь в программу, как показано в этом примере.

Тогда надо попробовать.

OMG, что это: action.activate += on_file_open_activate? Это так тут выглядит вызов обработчика сигнала? А аргументы куда?

Прочитал весь пост два раза и все равно не понял. Ты код на vala сам написал? Или оно сгенерировано? Если написал тогда ужос этот ваш vala :). В Qt все это делает moc, причем генерируя идентификаторы таблицы слотов/сигналов автоматически (то есть сравниваются уже не строки).

action.activate += on_file_open_activate -- это я присоединяю обработчик сигнала к соответствующему объекту. Аргументы указывать не надо, потому что в Vala строгая типизация и сигнатура сигнала уже известна. В данном случае сигнатура у нас public signal void activate();, то есть, без параметров (в этом случае self у нас уже есть). На самом деле, если вместо существующей функции указывать лямбда-функцию, как с open_file, то там первым аргументом всегда будет объект, сигнал которого обрабатывается.

В Qt ты загружаешь описание интерфейса в run-time? По-моему, оно там только compile-time. Здесь загрузка идет в run-time, от того и привязки вручную. Никто не мешает написать примитивную программу, которая будет читать описание интерфейса в Glade или Gtk.Builder и генерировать программу на C или Vala, чтобы был compile-time.


Отвечу сам себе: run-time загрузка есть, работает она ровно также, как и Gtk.Builder и при автоматическом соединении сигналов с обработчиками требует специального наименования обработчиков в духе on_[Widget]_[Signal]. Для Gtk.Builder такой написан, он берет символы в приложении и связывает их с по именам в описании в интерфейсе. Этот метод присоединения обработчиков используется по умолчанию.

Edited at 2008-08-21 10:44 pm (UTC)

  • 1
?

Log in

No account? Create an account