общая информация

Как это было в GTK 3

В GTK 3 деревья были реализованы с помощью виджета. GtkTreeViewGenericName и интерфейсные модели 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 предназначен для создания виджетов и привязки к данным, как описано в файлах пользовательского интерфейса, которые представляют собой описание интерфейса в формате XML. GtkSignalListItemFactory позволяет создавать и привязывать виджеты к данным через сигналы, подходящие для ручного создания виджетов в c.

Для реализации выбора строки шаблон выбора задается через интерфейс GtkSelectionModel. В GTK 4.10 доступны 3 модели выбора: GtkNoSelection, GtkSingleSelection И GtkMultiSelection. Какая модель для чего предназначена, понятно из названий классов (без возможности выбора, выбор одной строки, выбор нескольких строк).

GtkListView на основе дерева

Для реализации деревьев есть отдельная реализация интерфейса GListModel уполномоченный GtkTreeListМодель. В этой модели используется уже существующий объект, хранящий данные (можно использовать тип GListStore GLib), но позволяет добавлять дочерние строки к строкам по запросу с помощью функции обратного вызова. К этому моменту уже становится понятно, что новая схема работы с данными позволяет реализовать ленивый механизм загрузки данных из коробки. Функция обратного вызова для заполнения дочерних строк должна создать свою собственную GListStore, заполните его дочерними элементами и верните. Работает GtkTreeListModel объекты класса GtkTreeListRowкоторые просто хранят указатель на список дочерних объектов.

Сокращение и расширение ветки дерева выполняется с помощью GtkTreeExpanderкоторый также работает с моделью GtkTreeListModel.

Создайте дерево с помощью программирования

В этом разделе вы можете обратиться к примеру, который я написал для демонстрации возможностей GTK 4 в плане создания дерева. Исходный код проекта можно посмотреть в репозитории GitLab. демонстрация gtksqlite (Лицензия 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который выполняется перед уничтожением виджетов.

ЧИТАТЬ   API GPT-3.5 и его реализация в боте Telegram. Сравнение с ГПТ-3

Теперь создадим само дерево, указав для него модель выбора и фабрику виджетов:

    main_ui.tree_view = gtk_list_view_new(
            GTK_SELECTION_MODEL(tree_view_selection), tree_factory);

Наконец, нам нужно назначить обработчик события, чтобы включить линию (двойной щелчок или нажатие Enter) для развертывания ветвей дерева:

// Обработчик сигнала активации строки дерева:
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 вы можете обратиться к официальному Руководство и ГТК.

Используемые источники

  1. Масштабируемые списки в GTK 4 // Блог разработчиков GTK: все о GTK. — Дата обращения: 5 июня 2023 г.

  2. Отображение деревьев // Предпросмотр списка виджетов. – (Официальная документация GTK 4). — Дата обращения: 5 июня 2023 г.

  3. gliststore.c // Глиб. — (Репозиторий с исходным кодом GLib). — Дата обращения: 7 июня 2023 г.

Source

От admin