Реализация дерева на GtkListView в GTK 4
Общие сведения
Как было в GTK 3
В GTK 3 деревья реализовывались посредством виджета GtkTreeView и модели на основе интерфейса GtkTreeModel, предоставляющего для виджета данные. За отрисовку отвечали специальные рендереры ячеек (GtkCellRenderer, которые можно было назначать колонкам, в том числе в одну колонку можно было поместить несколько таких рендереров. Рендерер мог отрисовывать текстовое поле, флажок, картинку, индикатор прогресса или спиннер. Традиционным подходом к оформлению виджетов было добавление для колонок GtkTreeViewColumn атрибутов рендерерам, которые указывали, из какого столбца модели данных брать цвет фона ячейки, из какой — цвет текста и т. д. Для рендереров можно было также назначить цвет фона через свойство cell-background
самого рендерера.
Подобный подход к оформлению деревьев был и его основным недостатком. Рендереры не являются виджетами, не ведут себя как виджеты и не настраиваются через CSS в отличие от виджетов[1]. С ними возникало много сложностей в случае необходимости реализации чего-либо нестандартного. А в случае необходимости задействования CSS приходилось получать значение стиля из CSS-провайдера и вручную применять его к строкам таблицы или дерева (через атрибуты и колонки). Тем не менее, рендереры позволяли осуществлять достаточно быструю отрисовку информации, а ещё на них были основаны и некоторые другие виджеты, например, GtkComboBox (выпадающий список).
Объявление GtkTreeView устаревшим
В GTK 4.10 виджет GtkTreeView
(как и всё сопутствующее) был объявлен устаревшим. Сложный и громоздкий подход к построению деревьев решили упростить в пользу использования обычных виджетов. В качестве замены предлагается использовать виджеты GtkListView и GtkColumnView, которые сами по себе из коробки формировать деревья не умеют, но с точки зрения прикладного интерфейса используют более простую схему с обычными виджетами для отображения данных[2]. Хотя предложенная схема более простая с точки зрения поддержки, для реализации деревьев она всё ещё сложна, поскольку деревья теперь реализуются посредством дополнительных механизмов.
Как работает GtkListView
GtkListView
отображает виджеты на основе списков, реализующих интерфейс GListModel. Каждый элемент списка представляет собой какой-либо набор данных. Для отображения данных необходимы виджеты. Виджеты создаются через фабрики, наследующиеся от класса GtkListItemFactory. То есть для каждой отображаемой строки запускается создание виджета через фабрику. Затем виджету задаётся значение из строки с моделью данных, опять же через фабрику. Основной идеей является переиспользование виджетов, то есть одному и тому же виджету в разное время могут назначаться разные строки с данными. Именно за это и отвечают фабрики.
В версии GTK 4.10 существуют две фабрики: GtkBuilderListItemFactory и GtkSignalListItemFactory. Фабрика GtkBuilderListItemFactory
предназначена для создания виджетов и привязке к данным по описанию, сделанному в UI-файлах, которые представляют собой описание интерфейса в формате XML. GtkSignalListItemFactory
позволяет создавать и привязывать к данным виджеты через сигналы, что подходит для создания виджетов вручную в c-файлах.
Для реализации выделения строк задаётся модель выделения через интерфейс GtkSelectionModel. В версии GTK 4.10 доступны 3 модели выделения: GtkNoSelection, GtkSingleSelection и GtkMultiSelection. Какая модель для чего предназначена, понятно исходя из названий классов (без возможности выделения, выделение одной строки, выделение нескольких строк).
Дерево на основе GtkListView
Для реализации деревьев существует отдельная реализация интерфейса GListModel под названием GtkTreeListModel. Данная модель использует уже существующий объект, хранящий данные (можно использовать тип GListStore библиотеки GLib), но позволяет по запросу добавлять к строкам дочерние строки посредством функции обратного вызова. На этом моменте уже становится понятно, что новая схема работы с данными позволяет из коробки реализовывать механизм «ленивой» загрузки данных. Функция обратного вызова для заполнения дочерних строк должна создавать свой собственный GListStore
, заполнять его дочерними элементами и возвращать. Оперирует GtkTreeListModel
объектами класса GtkTreeListRow
, которые как раз и хранят в себе указатель на список дочерних объектов.
Сворачивание и разворачивание ветви дерева осуществляется при помощи GtkTreeExpander, который также работает с моделью GtkTreeListModel
.
Создаём дерево программно
В данном разделе можно ориентироваться на пример, написанный мною для демонстрации возможностей GTK 4 в плане создания деревьев. Исходный код проекта можно посмотреть в репозитории GitLab gtksqlite-demo (лицензия LGPL 2.1).
Для начала необходимо создать модель данных с помощью конструктора g_list_store_new()
, указав в качестве аргумента идентификатор типа данных (число типа gulong
), который будет храниться в строках. Создаётся впечатление, что в списке можно хранить G_TYPE_INT
, G_TYPE_STRING
или G_TYPE_ARRAY
, но внутри g_list_store_append()
выполняется g_object_ref()
[3], что предполагает добавление лишь объектов класса GObject
(с идентификатором типа G_TYPE_OBJECT
) или объектов классов-наследников. Конечно, можно попытаться добавлять туда произвольные данные, но это приведёт к неопределённому поведению (может скрешится, а может и нет) и ругательствам в поток вывода (в консоль). Регистрация собственных классов осуществляется с помощью макроса G_DEFINE_TYPE()
или его вариаций. Самым примитивным выбором для хранения строк с данными будет использование GObject
и добавление к нему данных через g_object_set_data()
/g_object_set_data_full()
. В таком случае можно обращаться к колонкам с данными по строковым ключам (ключи будут преобразовываться в кварки с помощью g_quark_from_string()
на глобальном уровне, хеш будет по кваркам). В более сложных случаях можно наследоваться от GObject
и реализовывать дополнительный функционал (например, сигналы на изменение данных).
Следует отметить, что GListStore
не завладевает добавленными в него объектами (выполняет g_object_ref()
, а не g_object_ref_sink()
), поэтому после добавления объекта через g_list_store_append()
необходимо самостоятельно делать объекту g_object_unref()
. Иначе будут утечки памяти, которые будет сложно выявить из-за механизма счётчика ссылок и наличия основного цикла программы.
Далее необходимо создать модель данных для дерева с помощью конструктора gtk_tree_list_model_new()
, передав ему список с данными строк дерева и указав в качестве обработчика заполнения дочерних элементов свою собственную функцию:
static GListModel *create_list_model_cb(gpointer item, gpointer user_data)
{
// Создаём список для хранения элементов собственного типа GtkDbRow
GListStore *list_store = g_list_store_new(G_TYPE_DB_ROW);
// Заполнение list_store какими-либо данными
// ...
return G_LIST_MODEL(list_store);
}
// ...
GtkTreeListModel *model =
gtk_tree_list_model_new(G_LIST_MODEL(main_ui.tree_store),
FALSE, // иначе дерево работать не будет
FALSE, // для динамической подгрузки
create_list_model_cb, // обработчик для создания дочерних элементов
NULL,
NULL);
Следующим этапом создаётся модель выделения, в нашем случае выделять можно будет лишь одну строку дерева:
GtkSingleSelection *tree_view_selection =
gtk_single_selection_new(G_LIST_MODEL(model));
Для отображения данных в дереве требуется фабрика. Ей необходимо будет назначить обработчики на создание виджетов (сигнал setup
) и на привязку к виджетам данных (сигнал bind
):
// Обработчик создания виджетов:
static void tree_list_item_setup_cb(GtkListItemFactory *factory,
GtkListItem *list_item,
gpointer user_data)
{
// Создаём виджет для раскрытия ветви дерева:
GtkWidget *tree_expander = gtk_tree_expander_new();
gtk_list_item_set_child(list_item, tree_expander);
// Контейнер для дочерних виджетов строки (если их более одного):
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
// Создание дочерних виджетов, каждому из которых будет назначена колонка из строки
// ...
// Назначаем виджету раскрытия ветви дочерний виджет:
gtk_tree_expander_set_child(GTK_TREE_EXPANDER(tree_expander), box);
}
// Обработчик привязки виджетов к данным:
static void tree_list_item_bind_cb(GtkListItemFactory *factory,
GtkListItem *list_item,
gpointer user_data)
{
GtkTreeListRow *tree_row = gtk_list_item_get_item(list_item);
GtkWidget *tree_expander = gtk_list_item_get_child(list_item);
// Назначаем виджету раскрытия текущую строку:
gtk_tree_expander_set_list_row(GTK_TREE_EXPANDER(tree_expander), tree_row);
// Получаем дочерние виджеты:
GtkWidget *box =
gtk_tree_expander_get_child(GTK_TREE_EXPANDER(tree_expander));
// ...
// Установка значений и сигналов для виджетов
// g_signal_connect_data(...)
}
// Обработчик отвязки данных от виджета:
static void tree_list_item_unbind_cb(GtkSignalListItemFactory *self,
GtkListItem *list_item,
gpointer user_data)
{
GtkTreeListRow *tree_row = gtk_list_item_get_item(list_item);
GtkWidget *tree_expander = gtk_list_item_get_child(list_item);
// Получаем дочерние виджеты:
GtkWidget *box =
gtk_tree_expander_get_child(GTK_TREE_EXPANDER(tree_expander));
// ...
// Отсоединение назначенных виджетам сигналов
// через g_signal_handlers_disconnect_matched() или иную функцию;
}
// ...
// Создаём фабрику и назначаем ей обработчики сигналов:
GtkListItemFactory *tree_factory = gtk_signal_list_item_factory_new();
g_signal_connect(
tree_factory, "setup", G_CALLBACK(tree_list_item_setup_cb), NULL);
g_signal_connect(
tree_factory, "bind", G_CALLBACK(tree_list_item_bind_cb), NULL);
g_signal_connect(
tree_factory, "unbind", G_CALLBACK(tree_list_item_unbind_cb), NULL);
// ...
Сигнал unbind
необходим для корректной отвязки данных от виджета. Например, если назначаются какие-либо обработчики событий, то их необходимо будет отвязать перед следующей привязкой новых данных (иначе могут остаться назначенными и старые, и новые обработчики, что приведёт к неопределённому поведению программы или утечке обработчиков сигналов или памяти). Необходимо заметить, что в сигнале unbind
объект типа GtkTreeListRow уже не будет привязан к данным строки (функция gtk_tree_list_row_get_item()
будет возвращать NULL
). В особых случаях также может понадобиться использовать сигнал teardown
, который выполняется перед уничтожением виджетов.
Теперь создадим непосредственно само дерево, указав ему модель выделения и фабрику для управления виджетами:
main_ui.tree_view = gtk_list_view_new(
GTK_SELECTION_MODEL(tree_view_selection), tree_factory);
Наконец, необходимо назначить обработчик события на активацию строки (двойной клик или нажатие ввода), чтобы разворачивать ветви дерева:
// Обработчик сигнала активации строки дерева:
static void
listview_activate_cb(GtkListView *list, guint position, gpointer unused)
{
GListModel *list_model = G_LIST_MODEL(gtk_list_view_get_model(list));
GtkTreeListRow *tree_row = g_list_model_get_item(list_model, position);
// Раскрываем или сворачиваем ветвь дерева:
gtk_tree_list_row_set_expanded(tree_row,
!gtk_tree_list_row_get_expanded(tree_row));
// ...
}
// ...
// Назначение обработчика на сигнал активации строки:
g_signal_connect(main_ui.tree_view,
"activate",
G_CALLBACK(listview_activate_cb), // указатель на обработчик
NULL);
При выделении элемента дерева может потребоваться выполнять какие-либо действия, например, заполнять данными таблицу со строками GtkColumnView
или заполнять данными какую-либо форму. Обработчик на выделение строки вешается не на виджет типа GtkListView
, а на модель выделения. В нашем случае — на объект типа GtkSingleSelection
. Данный тип наследуется от GtkSelectionModel
, в котором есть сигнал selection-changed
. Внутри обработчика изменения выделения получить выделенный элемент можно через метод gtk_single_selection_get_selected_item()
.
// Обработчик сигнала изменения выделения:
static void selection_changed_cb(GtkSingleSelection *selection_model,
guint start_position,
guint count,
gpointer user_data)
{
GtkTreeListRow *tree_row =
gtk_single_selection_get_selected_item(selection_model);
// Получаем саму строку с данными:
gpointer row_data = gtk_tree_list_row_get_item(tree_row);
// ...
}
// ...
// Назначаем обработчик сигнала изменения выделения:
g_signal_connect(tree_view_selection,
"selection-changed", // название сигнала
G_CALLBACK(selection_changed_cb), // обработчик
NULL);
Разумеется, созданное дерево необходимо добавить в GtkScrolledWindow
, который, в свою очередь, через какие-либо другие контейнеры необходимо поместить в окно приложения. Но в данной статье основы создания приложения на GTK не рассматриваются. По созданию простейшего приложения на GTK 4 можно обратиться к официальному мануалу GTK.
Использованные источники
Scalable lists in GTK 4 // GTK Development Blog: All things GTK. — Дата обращения: 5 июня 2023.
Displaying trees // List Widget Overview. — (Официальная документация GTK 4). — Дата обращения: 5 июня 2023.
gliststore.c // GLib. — (Репозиторий с исходным кодом GLib). — Дата обращения: 7 июня 2023.