Разработка приложений
Приложение состоит из нескольких файлов:
Исполняемый файл | Он устанавливается в /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");
}
...
Вы, возможно, заметили, что мы использовали вариант функции, устанавливающий шаблон. Теперь нам нужно использовать функциональность ресурсов 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 предоставляет макросы для этого в проектах, основанных на autotools.
Далее, нам нужно подключить наши настройки к виджетам, которые они должны контролировать. Один удобный способ сделать это — использовать функциональность привязки 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[]) { returng_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.20/ch01s04.html