Tree implementation on GtkListView in GTK 4

General information

As it was in GTK 3

In GTK 3, trees were implemented using a widget. GtkTreeView and interface-based models GtkTreeModelAn that provides data for the widget. Special cell renderers were responsible for rendering (GtkCellRenderer, which could be assigned to columns, including several such renderers could be placed in one column. The renderer could render a text field, a checkbox, an image, a progress bar, or a spinner. The traditional approach to styling widgets has been to add columns GtkTreeViewColumn attributes to renderers, which indicated which column of the data model to take the background color of the cell from, which one to take the text color from, etc. For renderers, it was also possible to assign a background color through the property cell-background the renderer itself.

A similar approach to the design of trees was also its main drawback. Renderers are not widgets, do not behave like widgets, and are not configurable via CSS unlike widgets[1]. There were a lot of difficulties with them if it was necessary to implement something non-standard. And if you need to use CSS, you had to get the style value from the CSS provider and manually apply it to the rows of the table or tree (via attributes and columns). Nevertheless, renderers allowed for fairly fast rendering of information, and some other widgets were also based on them, for example, GtkComboBox (drop-down list).

Deprecating GtkTreeView

In GTK 4.10 widget GtkTreeView (like everything related) has been deprecated. It was decided to simplify the complex and cumbersome approach to building trees in favor of using ordinary widgets. As a replacement, it is proposed to use widgets GtkListView And GtkColumnViewwhich by themselves do not know how to form trees out of the box, but from the point of view of the application interface, they use a simpler scheme with ordinary widgets to display data[2]. Although the proposed scheme is simpler in terms of support, it is still difficult to implement trees because trees are now implemented through additional mechanisms.

How GtkListView Works

GtkListView displays list-based widgets that implement an interface GListModel. Each element of the list represents a set of data. Widgets are required to display data. Widgets are created through factories that inherit from the class GtkListItemFactory. That is, for each displayed row, the creation of the widget through the factory is started. Then the widget is given a value from the string with the data model, again through the factory. The main idea is the reuse of widgets, that is, different rows with data can be assigned to the same widget at different times. That’s what factories are for.

There are two factories in GTK 4.10: GtkBuilderListItemFactory And GtkSignalListItemFactory. Factory GtkBuilderListItemFactory is designed to create widgets and bind to data according to the description made in UI files, which are a description of the interface in XML format. GtkSignalListItemFactory allows you to create and bind widgets to data via signals, which is suitable for creating widgets manually in c-files.

To implement the selection of lines, the selection model is specified through the interface GtkSelectionModel. There are 3 selection models available in GTK 4.10: GtkNoSelection, GtkSingleSelection And GtkMultiSelection. Which model is intended for what, it is clear based on the names of the classes (without the possibility of selection, selection of one line, selection of several lines).

Tree based GtkListView

To implement trees, there is a separate implementation of the interface GListModel entitled GtkTreeListModel. This model uses an already existing object that stores data (you can use the type GListStore GLib library), but allows child strings to be added to strings on demand via a callback function. At this point, it already becomes clear that the new scheme for working with data allows you to implement the lazy data loading mechanism out of the box. The callback function for padding child rows should create its own GListStore, populate it with child elements, and return. Operates GtkTreeListModel class objects GtkTreeListRowwhich just store a pointer to a list of child objects.

Collapsing and expanding a tree branch is done using GtkTreeExpanderwhich also works with the model GtkTreeListModel.

Create a tree programmatically

In this section, you can refer to an example I wrote to demonstrate the possibilities of GTK 4 in terms of creating trees. The source code of the project can be viewed in the GitLab repository gtksqlite-demo (license LGPL 2.1).

First you need to create a data model using the constructor g_list_store_new()specifying the identifier of the data type as an argument (the number of the type gulong), which will be stored in strings. It seems that the list can store G_TYPE_INT, G_TYPE_STRING or G_TYPE_ARRAYbut inside g_list_store_append() performed g_object_ref()[3]which involves adding only class objects GObject (with type identifier G_TYPE_OBJECT) or objects of derived classes. Of course, you can try to add arbitrary data there, but this will lead to undefined behavior (may be skewed, or maybe not) and curses in the output stream (to the console). Registration of own classes is carried out using a macro G_DEFINE_TYPE() or its variations. The most primitive choice for storing rows of data would be to use GObject and adding data to it via g_object_set_data()/g_object_set_data_full(). In this case, you can access columns with data by string keys (keys will be converted to quarks using g_quark_from_string() globally, the hash will be by quarks). In more complex cases, you can inherit from GObject and implement additional functionality (for example, data change signals).

It should be noted that GListStore does not take possession of the objects added to it (performs g_object_ref()but not g_object_ref_sink()), so after adding the object via g_list_store_append() you need to do the object yourself g_object_unref(). Otherwise, there will be memory leaks that will be difficult to detect due to the reference counter mechanism and the presence of the main program loop.

Next, you need to create a data model for the tree using the constructor gtk_tree_list_model_new()passing it a list of tree row data and specifying your own function as the child fill handler:

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);

The next step is to create a selection model, in our case it will be possible to select only one line of the tree:

    GtkSingleSelection *tree_view_selection =
            gtk_single_selection_new(G_LIST_MODEL(model));

A factory is required to display data in a tree. It will need to assign handlers to create widgets (signal setup) and binding to data widgets (signal 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);

// ...

Signal unbind necessary for the correct unbinding of data from the widget. For example, if any event handlers are assigned, they will need to be unbind before the next binding of new data (otherwise, both old and new handlers may remain assigned, leading to undefined program behavior or leaking signal handlers or memory). It should be noted that in the signal unbind an object of type GtkTreeListRow will no longer be bound to row data (function gtk_tree_list_row_get_item() will return NULL). In special cases it may also be necessary to use the signal teardownwhich is executed before the widgets are destroyed.

Now let’s create the tree itself, specifying the selection model and the widget factory for it:

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

Finally, we need to assign an event handler to activate the row (double click or press enter) to expand the branches of the tree:

// Обработчик сигнала активации строки дерева:
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);

When selecting a tree element, it may be necessary to perform some actions, for example, fill a table with rows with data GtkColumnView or fill out a form with data. The handler for selecting a line is not attached to a widget of type GtkListView, but on the selection model. In our case, on an object of type GtkSingleSelection. This type is inherited from GtkSelectionModelwhich has a signal selection-changed. Inside the selection change handler, you can get the selected element through the method 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);

Of course, the created tree must be added to GtkScrolledWindow, which, in turn, must be placed in the application window through some other containers. But this article does not cover the basics of creating a GTK application. To create a simple application on GTK 4, you can refer to the official GTK manual.

Used sources

  1. Scalable lists in GTK 4 // GTK Development Blog : All things GTK. — Date of access: 5 June 2023.

  2. display trees // List Widget Overview. – (Official documentation of GTK 4). — Date of access: 5 June 2023.

  3. gliststore.c // GLib. — (Repository with GLib source code). — Date of access: 7 June 2023.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *