Bloerg
       

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:

2012-10-23/test-1.png

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

2012-10-23/test-2.png

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

2012-10-23/test-3.png

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.

Discussion

hannenz
Fri, Aug 30 2013

Very useful, thanks a lot. Do you know how to make a tree view sortable by multiple columns (e.g. sort by price AND name) ?

Matthias
Sat, Aug 31 2013

You can define a custom sort function and set it with gtk_tree_sortable_set_sort_func. The sort function receives two TreeIters from which you can get the model data and calculate the desired sort ordering.

MattT
Fri, Sep 27 2013

Have you had any luck dynamically adding rows and having them properly sort? I can add rows just fine, but they always seem to be at the beginning or end of the treeview, no matter what column/direction I am sorting by. The only way I’ve found to achieve proper sorting is by replacing the entire model. I must be missing something simple.

Matthias
Fri, Sep 27 2013

I have no problem adding new items dynamically which are sorted according to the currently selected column. I just tried and added a button which is connected to this handler:

static void
on_addbutton_clicked (GtkButton *button, Store *store)
{
    GtkTreeIter iter;
    gchar *item_name;

    item_name = g_strdup_printf ("%c article", (gchar) g_random_int_range (65, 90));
    gtk_list_store_append (store->articles, &iter); 
    gtk_list_store_set (store->articles, &iter, COLUMN_ARTICLE, item_name,
                        COLUMN_PRICE, g_random_double_range (0.50, 10.0), -1);

    g_free (item_name);
}

Depending on which column is selected for ordering, the items are shown in the correct place.

MattT
Fri, Sep 27 2013

Hmm. I’m using GTK#, which seems much less 1-to-1 with GTK than other wrappers I’ve previously used (PyGTK, for instance). I’m guessing the issue is either with that API or the specific calls I’m using.

Thanks for confirming this should just work as expected.

MattT
Fri, Sep 27 2013

Sorry to spam you with GTK# related stuff, but I thought this could be useful to others that hit my issue.

I was using the GTK# convenience method that is used in the examples in their docs. Like this:

store.AppendValues(item1, item2);

It turns out that this does not honor any sorting and filtering that you have setup on the store and just places it, seemingly arbitrarily, at the end or beginning of the store. Using the slightly-less-convenient append/set value (as in the examples for GTK here) works properly with sorting. Like this:

TreeIter iter = store.Append();
store.SetValue(iter, 0, item1);
iter = store.Append();
store.SetValue(iter, 0, item2);

Thanks again for the nudge in the right direction.

Matthias
Fri, Sep 27 2013

Glad you found the problem. Maybe you should report that errornous behaviour upstream.

Allen Pan
Tue, Mar 25 2014

Good Share ! Very useful for me

Post a comment

Name required

E-mail required, not published

Website optional

Comment Markdown accepted


This post might also have some comments at Google+.