A Sorted and Filtered GtkTreeView

For a project yet-to-be-announced, I needed a GtkTreeView that can sort the underlying data by clicking on the column header and filter out rows satisfying a certain condition. Both problems can be solved independently rather quickly but it took me some time and a hint on the Gtk+ mailing list to implement both at the same time. For your pleasure and my fading brain activity, I will outline the steps necessary to implement a sortable and filtered tree view in C.

First of all, we need a multi-columned data store to keep our data. For this example let’s assume a database that stores articles and their corresponding prices. We want to sort them by their name and price and we also want to filter out all articles that exceed a certain price. For this, we define symbols to access the columns by name rather than a magic number:

typedef struct
{
    GtkListStore       *articles;
    GtkTreeModelSort   *sorted;
    GtkTreeModelFilter *filtered;
    gdouble             max_price;
} Store;

enum
{
    COLUMN_ARTICLE = 0,
    COLUMN_PRICE,
    N_COLUMNS
};

We create the store with gtk_list_store_new and add some rows:

GtkTreeIter iter;
Store *store;

store = g_new0 (Store, 1);
store->articles = gtk_list_store_new (N_COLUMNS,
                                      G_TYPE_STRING,
                                      G_TYPE_DOUBLE);

gtk_list_store_append (store->articles, &iter);
gtk_list_store_set (store->articles, &iter,
                    COLUMN_ARTICLE, "Spam",
                    COLUMN_PRICE, 1.20, -1);

gtk_list_store_append (store->articles, &iter);
gtk_list_store_set (store->articles, &iter,
                    COLUMN_ARTICLE, "Beer",
                    COLUMN_PRICE, 5.99, -1);

gtk_list_store_append (store->articles, &iter);
gtk_list_store_set (store->articles, &iter,
                    COLUMN_ARTICLE, "Chewing Gum",
                    COLUMN_PRICE, 0.99, -1);

By itself, a GtkListStore can not be filtered and sorted. However, the tree model subclasses GtkTreeModelFilter and GtkTreeModelSort wrap a child model that export a reduced and sorted data set. In order to implement both behaviours, the articles model becomes the child of the filtered model and the filtered model the child of the sorted model:

store->filtered = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (GTK_TREE_MODEL (store->articles), NULL));
store->sorted = GTK_TREE_MODEL_SORT (gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (store->filtered)));

The filtered tree model does not know which rule is in place to filter out rows. For this example, we set a filter callback function

gtk_tree_model_filter_set_visible_func (store->filtered,
                                        (GtkTreeModelFilterVisibleFunc) row_visible,
                                        store, NULL);

that returns TRUE for all rows that exceed the maximum price:

static gboolean
row_visible (GtkTreeModel *model,
             GtkTreeIter *iter,
             Store *store)
{
    gdouble price;

    gtk_tree_model_get (model, iter, COLUMN_PRICE, &price, -1);
    return price <= store->max_price;
}

Our data model is ready. Now, we need to create a GtkTreeView for the sorted model to show it to the user:

GtkTreeView *view;
GtkTreeModel *model;

model = GTK_TREE_MODEL (store->sorted);
view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (model));

To define which columns we want to show, we create GtkTreeViewColumn objects and add them to the view. A GtkCellRenderer is responsible for how a GtkTreeViewColumn is presented. In our case, we create a simple text renderer for the article and price column:

GtkCellRenderer     *renderer;
GtkTreeViewColumn   *article_column;
GtkTreeViewColumn   *price_column;

renderer = gtk_cell_renderer_text_new ();

article_column = gtk_tree_view_column_new_with_attributes (
        "Article", renderer,
        "text", COLUMN_ARTICLE,
        NULL);

gtk_tree_view_append_column (view, article_column);

price_column = gtk_tree_view_column_new_with_attributes (
        "Price", renderer,
        "text", COLUMN_PRICE,
        NULL);

gtk_tree_view_append_column (view, price_column);

Now, this is what we have got so far:

Howerver, we don’t just want to present the data but also let the user interact with it. To implement the well-known double click on a row paradigm, we react on the "row-activated" signal of the view:

g_signal_connect (view, "row-activated",
                  G_CALLBACK (on_row_activated), store);

Now, what happens when a user clicks on a row that has been shuffled around because of all the filtering and sorting? Using the GtkTreeIter on our full, we will probably select the wrong row. Thus, we have to undo the path conversion that was introduced by the filter and sort model:

static void
on_row_activated (GtkTreeView *view,
                  GtkTreePath *path,
                  GtkTreeViewColumn *col,
                  Store *store)
{
    GtkTreeIter iter;
    GtkTreePath *filtered_path;
    GtkTreePath *true_path;

    /* 
     * We have a path that is filtered first and then sorted. So first, let's
     * undo the sort.
     */
    filtered_path = gtk_tree_model_sort_convert_path_to_child_path (GTK_TREE_MODEL_SORT (store->sorted),
                                                                    path);

    /*
     * Then we undo the filter and have the path to the correct row.
     */
    true_path = gtk_tree_model_filter_convert_path_to_child_path (GTK_TREE_MODEL_FILTER (store->filtered),
                                                                   filtered_path);

    if (gtk_tree_model_get_iter (GTK_TREE_MODEL (store->articles), &iter, true_path)) {
        gchar *article;
        gdouble price;

        gtk_tree_model_get (GTK_TREE_MODEL (store->articles), &iter,
                            COLUMN_ARTICLE, &article,
                            COLUMN_PRICE, &price,
                            -1);

        g_print ("You want to buy %s for %f?\n", article, price);
        g_free (article);
    }
}

We also specify which columns can be sorted and according to what model column they are sorted (yes, the column that is shown is not necessarily the same that is used to sort the rows):

gtk_tree_view_column_set_sort_column_id (article_column, COLUMN_ARTICLE);
gtk_tree_view_column_set_sort_column_id (price_column, COLUMN_PRICE);

Now, we hook everything up and put the view in a window. We also add a spin button to change the currently set maximum price:

GtkWidget *window;
GtkWidget *box;
GtkWidget *spinbutton;
GtkAdjustment *max_price;

window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);

max_price = gtk_adjustment_new (10.99, 0.01, 1024.0, 0.01, 1.0, 0.0);
spinbutton = gtk_spin_button_new (max_price, 1.0, 2);

gtk_container_add (GTK_CONTAINER (window), box);
gtk_container_add (GTK_CONTAINER (box), view);
gtk_container_add (GTK_CONTAINER (box), spinbutton);

g_signal_connect (G_OBJECT (window), "delete-event",
                  G_CALLBACK (gtk_main_quit), NULL);

g_signal_connect (G_OBJECT (max_price), "value-changed",
                  G_CALLBACK (on_max_price_changed), store);

Whenever we change the price, we need to set the corresponding variable and filter our list again:

static void
on_max_price_changed (GtkAdjustment *adjustment,
                      Store *store)
{
    store->max_price = gtk_adjustment_get_value (adjustment);
    gtk_tree_model_filter_refilter (store->filtered);
}

That’s it and these are the fruits of our labour: A filtered model with a reduced maximum price

and the output after interacting with the filtered and sorted model:

You can compile the complete example with

gcc `pkg-config --cflags gtk+-3.0` sorted-and-filtered-tree-view.c `pkg-config --libs gtk+-3.0`

Update: The reason why we have two calls to pkg-config is that most Gtk+ symbols are not referenced during the compilation step but need to at link stage. Thus we have to append the link flags rather than using them in front for the compiler.

If there are any doubts left, consult the excellent documentation for Tree and List Widgets on the gnome.org website.