Создание приложений
Приложение состоит из нескольких файлов:
Исполняемый файл | Он устанавливается в /usr/bin. |
Файл для рабочего стола | Файл рабочего стола предоставляет важную информацию о приложении для оболочки рабочего стола, такую как его имя, значок, имя D-Bus, командная строка для запуска и т. д. Он устанавливается в /usr/share/applications. |
Значок | Значок устанавливается в /usr/share/icons/hicolor/48x48/apps, где он будет найден независимо от текущей темы. |
Схема настроек | Если приложение использует GSettings, оно установит свою схему в /usr/share/glib-2.0/schemas, чтобы такие инструменты, как dconf-editor, могли ее найти. |
Другие ресурсы | Другие файлы, такие как файлы пользовательского интерфейса GtkBuilder, лучше загружать из ресурсов, хранящихся в самом исполняемом файле приложения. Это исключает необходимость большинства файлов, которые традиционно устанавливались в местоположение, специфичное для приложения, в /usr/share. |
GTK+ включает поддержку приложений, построенную поверх GApplication. В этом руководстве мы создадим простое приложение, начиная с нуля и постепенно добавляя всё больше деталей. По ходу дела мы узнаем о GtkApplication, шаблонах, ресурсах, меню приложения, настройках, GtkHeaderBar, GtkStack, GtkSearchBar, GtkListBox и многом другом.
Полные, компилируемые исходники этих примеров можно найти в каталоге examples дистрибутива GTK+, или в онлайн-репозитории GTK+ в Git. Вы можете скомпилировать каждый пример по отдельности, используя make с файлом Makefile.example. Дополнительную информацию см. в README, входящем в каталог примеров.
Тривиальное приложение
При использовании GtkApplication, функция main() может быть очень простой. Мы просто вызываем g_application_run() и передаем ей экземпляр нашего класса приложения.
Вся логика приложения находится в классе приложения, который является подклассом GtkApplication. Наш пример пока не имеет какой-либо интересной функциональности. Всё, что он делает, это открывает окно при активации без аргументов и открывает файлы, если он запущен с аргументами.
Для обработки этих двух случаев мы переопределяем функцию activate(), которая вызывается при запуске приложения без аргументов командной строки, и функцию open(), которая вызывается при запуске приложения с аргументами командной строки.
Чтобы узнать больше о точках входа GApplication, обратитесь к документации GIO документации.
Еще один важный класс, который является частью поддержки приложений в GTK+, — это GtkApplicationWindow. Он, как правило, также наследуется. Наш подкласс пока ничего не делает, поэтому мы получим пустое окно.
В качестве начальной настройки нашего приложения мы также создаем значок и файл для рабочего стола.
Обратите внимание, что @bindir@ необходимо заменить фактическим путем к исполняемому файлу перед использованием этого файла для рабочего стола.
Вот что мы достигли на данный момент:
Это пока не выглядит очень впечатляюще, но наше приложение уже представляет себя в сеансе, оно имеет семантику единственного экземпляра и принимает файлы в качестве аргументов командной строки.
Заполнение окна
На этом шаге мы используем шаблон GtkBuilder для сопоставления файла пользовательского интерфейса GtkBuilder с классом окна нашего приложения.
Наш простой файл пользовательского интерфейса размещает GtkHeaderBar поверх виджета GtkStack. Панель заголовка содержит GtkStackSwitcher, который является самостоятельным виджетом для отображения ряда «вкладок» для страниц GtkStack.
Для использования этого файла в нашем приложении мы возвращаемся к нашему подклассу GtkApplicationWindow и вызываем gtk_widget_class_set_template_from_resource() из функции инициализации класса, чтобы установить файл пользовательского интерфейса в качестве шаблона для этого класса. Мы также добавляем вызов gtk_widget_init_template() в функцию инициализации экземпляра для создания шаблона для каждого экземпляра нашего класса.
...
static void
example_app_window_init (ExampleAppWindow *win)
{
gtk_widget_init_template (GTK_WIDGET (win));
}
static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
"/org/gtk/exampleapp/window.ui");
}
...
Вы могли заметить, что мы использовали вариант функции, устанавливающей шаблон _from_resource(). Теперь нам нужно использовать функциональность ресурсов GLib для включения файла пользовательского интерфейса в исполняемый файл. Это обычно делается путем перечисления всех ресурсов в файле .gresource.xml, например, в этом:
Этот файл должен быть преобразован в файл исходного кода C, который будет скомпилирован и связан с приложением вместе с другими исходными файлами. Для этого мы используем утилиту glib-compile-resources:
glib-compile-resources exampleapp.gresource.xml --target=resources.c --generate-source
Теперь наше приложение выглядит так:
Открытие файлов
На этом шаге мы заставим наше приложение отображать содержимое всех файлов, указанных в командной строке.
Для этого мы добавим структуру-член нашего подкласса окна приложения и сохраним ссылку на GtkStack там. Функция gtk_widget_class_bind_template_child_private() организует вещи так, что после создания шаблона член stack структуры-члена будет указывать на виджет с таким же именем из шаблона.
...
struct _ExampleAppWindowPrivate
{
GtkWidget *stack;
};
G_DEFINE_TYPE_WITH_PRIVATE(ExampleAppWindow, example_app_window, GTK_TYPE_APPLICATION_WINDOW);
...
static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
"/org/gtk/exampleapp/window.ui");
gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (class), ExampleAppWindow, stack);
}
...
Теперь мы возвращаемся к функции example_app_window_open(), которая вызывается для каждого аргумента командной строки, и создаем GtkTextView, который затем добавляем в качестве страницы в стопку:
...
void
example_app_window_open (ExampleAppWindow *win,
GFile *file)
{
ExampleAppWindowPrivate *priv;
gchar *basename;
GtkWidget *scrolled, *view;
gchar *contents;
gsize length;
priv = example_app_window_get_instance_private (win);
basename = g_file_get_basename (file);
scrolled = gtk_scrolled_window_new (NULL, NULL);
gtk_widget_show (scrolled);
gtk_widget_set_hexpand (scrolled, TRUE);
gtk_widget_set_vexpand (scrolled, TRUE);
view = gtk_text_view_new ();
gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (view), FALSE);
gtk_widget_show (view);
gtk_container_add (GTK_CONTAINER (scrolled), view);
gtk_stack_add_titled (GTK_STACK (priv->stack), scrolled, basename, basename);
if (g_file_load_contents (file, NULL, &contents, &length, NULL, NULL))
{
GtkTextBuffer *buffer;
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
gtk_text_buffer_set_text (buffer, contents, length);
g_free (contents);
}
g_free (basename);
}
...
Обратите внимание, что нам не пришлось ничего менять в переключателе стопок. Он получает всю свою информацию из стопки, к которой принадлежит. Здесь мы передаем метку для отображения каждого файла в качестве последнего аргумента функции gtk_stack_add_titled().
Наше приложение начинает обретать форму:
Меню приложения
Меню приложения отображается оболочкой GNOME в верхней части экрана. Оно предназначено для сбора редко используемых действий, которые влияют на всё приложение.
Точно так же, как и шаблон окна, мы определяем меню приложения в файле пользовательского интерфейса и добавляем его в качестве ресурса в наш исполняемый файл.
Для ассоциации меню приложения с приложением необходимо вызвать gtk_application_set_app_menu(). Поскольку меню приложения работают за счёт активации GActions, мы также должны добавить соответствующий набор действий к нашему приложению.
Обе эти задачи лучше всего выполнять в функции startup(), которая гарантированно вызывается один раз для каждого основного экземпляра приложения:
...
static void
preferences_activated (GSimpleAction *action,
GVariant *parameter,
gpointer app)
{
}
static void
quit_activated (GSimpleAction *action,
GVariant *parameter,
gpointer app)
{
g_application_quit (G_APPLICATION (app));
}
static GActionEntry app_entries[] =
{
{ "preferences", preferences_activated, NULL, NULL, NULL },
{ "quit", quit_activated, NULL, NULL, NULL }
};
static void
example_app_startup (GApplication *app)
{
GtkBuilder *builder;
GMenuModel *app_menu;
const gchar *quit_accels[2] = { "<Ctrl>Q", NULL };
G_APPLICATION_CLASS (example_app_parent_class)->startup (app);
g_action_map_add_action_entries (G_ACTION_MAP (app),
app_entries, G_N_ELEMENTS (app_entries),
app);
gtk_application_set_accels_for_action (GTK_APPLICATION (app),
"app.quit",
quit_accels);
builder = gtk_builder_new_from_resource ("/org/gtk/exampleapp/app-menu.ui");
app_menu = G_MENU_MODEL (gtk_builder_get_object (builder, "appmenu"));
gtk_application_set_app_menu (GTK_APPLICATION (app), app_menu);
g_object_unref (builder);
}
static void
example_app_class_init (ExampleAppClass *class)
{
G_APPLICATION_CLASS (class)->startup = example_app_startup;
...
}
...
Наш пункт меню «Настройки» пока ничего не делает, но пункт меню «Выход» полностью функционален. Обратите внимание, что его также можно активировать обычным сочетанием клавиш Ctrl-Q. Сочетание клавиш было добавлено с помощью gtk_application_set_accels_for_action().
Меню приложения выглядит так:
Диалог настроек
Типичное приложение имеет некоторые настройки, которые следует запоминать между запусками. Даже для нашего простого примера приложения, мы можем захотеть изменить шрифт, используемый для содержимого.
Мы будем использовать GSettings для хранения наших настроек. GSettings требует схемы, описывающей наши настройки:
Прежде чем мы сможем использовать эту схему в нашем приложении, нам нужно скомпилировать ее в двоичный формат, ожидаемый GSettings. GIO предоставляет макросы для этого в проектах на основе автоинструментов.
Далее, нам нужно связать наши настройки с виджетами, которые они должны контролировать. Один удобный способ сделать это — использовать функциональность привязки GSettings для привязки ключей настроек к свойствам объектов, как мы делаем здесь для настройки перехода.
...
static void
example_app_window_init (ExampleAppWindow *win)
{
ExampleAppWindowPrivate *priv;
priv = example_app_window_get_instance_private (win);
gtk_widget_init_template (GTK_WIDGET (win));
priv->settings = g_settings_new ("org.gtk.exampleapp");
g_settings_bind (priv->settings, "transition",
priv->stack, "transition-type",
G_SETTINGS_BIND_DEFAULT);
}
...
Код для связи настройки шрифта немного сложнее, так как нет простого свойства объекта, которому он соответствует, поэтому мы не будем углубляться в это здесь.
На этом этапе приложение уже будет реагировать на изменение одной из настроек, например, с помощью инструмента командной строки gsettings. Конечно, мы ожидаем, что приложение предоставит диалог настроек для этих настроек. Итак, давайте сделаем это сейчас. Наш диалог настроек будет подклассом GtkDialog, и мы будем использовать те же методы, которые мы уже видели: шаблоны, частные структуры, привязки настроек.
Давайте начнем с шаблона.
Далее идет подкласс диалога.
Теперь мы пересмотрим функцию preferences_activated() в нашем классе приложения и заставим её открывать новый диалог настроек.
...
static void
preferences_activated (GSimpleAction *action,
GVariant *parameter,
gpointer app)
{
ExampleAppPrefs *prefs;
GtkWindow *win;
win = gtk_application_get_active_window (GTK_APPLICATION (app));
prefs = example_app_prefs_new (EXAMPLE_APP_WINDOW (win));
gtk_window_present (GTK_WINDOW (prefs));
}
...
После всей этой работы наше приложение теперь может отображать диалог настроек следующим образом:
Добавление панели поиска
Мы продолжаем расширять функциональность нашего приложения. Сейчас мы добавим поиск. GTK+ поддерживает это с помощью GtkSearchEntry и GtkSearchBar. Панель поиска — это виджет, который может выезжать сверху, чтобы представить поле поиска.
Мы добавляем переключатель в строку заголовка, который может использоваться для выдвижения панели поиска под строкой заголовка.
Реализация поиска требует довольно много изменений в коде, которые мы не будем полностью рассматривать здесь. Центральной частью реализации поиска является обработчик сигнала, который прослушивает изменения текста в поле ввода поиска.
...
static void
search_text_changed (GtkEntry *entry,
ExampleAppWindow *win)
{
ExampleAppWindowPrivate *priv;
const gchar *text;
GtkWidget *tab;
GtkWidget *view;
GtkTextBuffer *buffer;
GtkTextIter start, match_start, match_end;
text = gtk_entry_get_text (entry);
if (text[0] == '\0')
return;
priv = example_app_window_get_instance_private (win);
tab = gtk_stack_get_visible_child (GTK_STACK (priv->stack));
view = gtk_bin_get_child (GTK_BIN (tab));
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
/* Very simple-minded search implementation */
gtk_text_buffer_get_start_iter (buffer, &start);
if (gtk_text_iter_forward_search (&start, text, GTK_TEXT_SEARCH_CASE_INSENSITIVE,
&match_start, &match_end, NULL))
{
gtk_text_buffer_select_range (buffer, &match_start, &match_end);
gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &match_start,
0.0, FALSE, 0.0, 0.0);
}
}
static void
example_app_window_init (ExampleAppWindow *win)
{
...
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), search_text_changed);
...
}
...
С панелью поиска наше приложение теперь выглядит так:
Добавление боковой панели
В качестве другой функциональности мы добавляем боковую панель, которая демонстрирует GtkMenuButton, GtkRevealer и GtkListBox.
Код для заполнения боковой панели кнопками для слов, найденных в каждом файле, немного слишком сложен, чтобы углубляться в него здесь. Но мы рассмотрим код для добавления меню шестеренки.
Как и ожидалось, меню шестеренки указано в файле пользовательского интерфейса GtkBuilder.
Для связи пункта меню с настройкой show-words мы используем GAction, соответствующий заданному ключу GSettings.
...
static void
example_app_window_init (ExampleAppWindow *win)
{
...
builder = gtk_builder_new_from_resource ("/org/gtk/exampleapp/gears-menu.ui");
menu = G_MENU_MODEL (gtk_builder_get_object (builder, "menu"));
gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (priv->gears), menu);
g_object_unref (builder);
action = g_settings_create_action (priv->settings, "show-words");
g_action_map_add_action (G_ACTION_MAP (win), action);
g_object_unref (action);
}
...
Вот как выглядит наше приложение сейчас:
Свойства
У виджетов и других объектов есть много полезных свойств.
Здесь мы показываем несколько способов их использования новыми и гибкими способами, обернув их в действия с помощью GPropertyAction или связав их с помощью GBinding.
Для настройки мы добавим две метки в строку заголовка в нашем шаблоне окна, названные lines_label и lines, и свяжем их с членами структуры в частной структуре, как мы уже видели несколько раз.
Мы добавляем новый пункт меню "Строки" в меню шестеренки, который запускает действие show-lines:
Чтобы заставить этот пункт меню что-то делать, мы создаем действие свойства для свойства visible метки lines и добавляем его к действиям окна. В результате видимость метки будет переключаться каждый раз, когда действие активируется.
Поскольку мы хотим, чтобы обе метки появлялись и исчезали вместе, мы связываем свойство visible виджета lines_label с тем же свойством виджета lines.
...
static void
example_app_window_init (ExampleAppWindow *win)
{
...
action = (GAction*) g_property_action_new ("show-lines", priv->lines, "visible");
g_action_map_add_action (G_ACTION_MAP (win), action);
g_object_unref (action);
g_object_bind_property (priv->lines, "visible",
priv->lines_label, "visible",
G_BINDING_DEFAULT);
}
...
Нам также нужна функция, которая подсчитывает строки в активной вкладке и обновляет метку lines. Смотрите полный исходный код, если вас интересуют подробности.
Это приводит наше примерное приложение к такому виду:
Строка заголовка
Наш приложение уже использует GtkHeaderBar, но пока что на нём всё ещё появляется стандартная строка заголовка окна сверху. Это немного избыточно, и теперь мы скажем GTK+, чтобы использовать строку заголовка как замену строки заголовка. Для этого мы переместим её в качестве непосредственного потомка окна и установим её тип в titlebar.
#include <gtk/gtk.h> #include "exampleapp.h" int main (int argc, char *argv[]) { return g_application_run (G_APPLICATION (example_app_new ()), argc, argv); }
Небольшим дополнительным преимуществом использования строки заголовка является то, что мы получаем стандартное меню приложения бесплатно. Вот как приложение теперь выглядит, если используется это меню.
Если мы настраиваем значок окна для нашего окна, кнопка меню будет использовать его вместо универсального значка-заполнителя, который вы видите здесь.
© 2005–2020 The GNOME Project
Licensed under the GNU Lesser General Public License version 2.1 or later.
https://developer.gnome.org/gtk3/3.22/ch01s04.html