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.