Last modified on 6 March 2011, at 04:13

GTK+ By Example/Tree View/Sorting

SortingEdit

Lists and trees are meant to be sorted. This is done using the GtkTreeSortable interface that can be implemented by tree models. 'Interface' means that you can just cast a GtkTreeModel into a GtkTreeSortable with GTK_TREE_SORTABLE(model) and use the documented tree sortable functions on it, just like we did before when we cast a list store to a tree model and used the gtk_tree_model_foo family of functions. Both GtkListStore and GtkTreeStore implement the tree sortable interface.

The most straight forward way to sort a list store or tree store is to directly use the tree sortable interface on them. This will sort the store in place, meaning that rows will actually be reordered in the store if required. This has the advantage that the position of a row in the tree view will always be the same as the position of a row in the model, in other words: a tree path referring to a row in the view will always refer to the same row in the model, so you can get a row's iter easily with gtk_tree_model_get_iter using a tree path supplied by the tree view. This is not only convenient, but also sufficient for most scenarios.

However, there are cases when sorting a model in place is not desirable, for example when several tree views display the same model with different sortings, or when the unsorted state of the model has some special meaning and needs to be restored at some point. This is where GtkTreeModelSort comes in, which is a special model that maps the unsorted rows of a child model (e.g. a list store or tree store) into a sorted state without changing the child model.

GtkTreeSortableEdit

The tree sortable interface is fairly simple and should be easy to use. Basically you define a 'sort column ID' integer for every criterion you might want to sort by and tell the tree sortable which function should be called to compare two rows (represented by two tree iters) for every sort ID with gtk_tree_sortable_set_sort_func. Then you sort the model by setting the sort column ID and sort order with gtk_tree_sortable_set_sort_column_id, and the model will be re-sorted using the compare function you have set up. Your sort column IDs can correspond to your model columns, but they do not have to (you might want to sort according to a criterion that is not directly represented by the data in one single model column, for example). Some code to illustrate this:

  enum
  {
    COL_NAME = 0,
    COL_YEAR_BORN
  };
 
 
  enum
  {
    SORTID_NAME = 0,
    SORTID_YEAR
  };
 
 
  GtkTreeModel  *liststore = NULL;
 
 
  void
  toolbar_onSortByYear (void)
  {
    GtkTreeSortable *sortable;
    GtkSortType      order;
    gint             sortid;
 
    sortable = GTK_TREE_SORTABLE(liststore);
 
    /* If we are already sorting by year, reverse sort order,
     *  otherwise set it to year in ascending order */
 
    if (gtk_tree_sortable_get_sort_column_id(sortable, &sortid, &order) == TRUE
          &&  sortid == SORTID_YEAR)
    {
      GtkSortType neworder;
 
      neworder = (order == GTK_SORT_ASCENDING) ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING;
 
      gtk_tree_sortable_set_sort_column_id(sortable, SORTID_YEAR, neworder);
    }
    else
    {
      gtk_tree_sortable_set_sort_column_id(sortable, SORTID_YEAR, GTK_SORT_ASCENDING);
    }
  }
 
 
  /* This is not pretty. Of course you can also use a
   *  separate compare function for each sort ID value */
 
  gint
  sort_iter_compare_func (GtkTreeModel *model,
                          GtkTreeIter  *a,
                          GtkTreeIter  *b,
                          gpointer      userdata)
  {
    gint sortcol = GPOINTER_TO_INT(userdata);
    gint ret = 0;
 
    switch (sortcol)
    {
      case SORTID_NAME:
      {
        gchar *name1, *name2;
 
        gtk_tree_model_get(model, a, COL_NAME, &name1, -1);
        gtk_tree_model_get(model, b, COL_NAME, &name2, -1);
 
        if (name1 == NULL || name2 == NULL)
        {
          if (name1 == NULL && name2 == NULL)
            break; /* both equal => ret = 0 */
 
          ret = (name1 == NULL) ? -1 : 1;
        }
        else
        {
          ret = g_utf8_collate(name1,name2);
        }
 
        g_free(name1);
        g_free(name2);
      }
      break;
 
      case SORTID_YEAR:
      {
        guint year1, year2;
 
        gtk_tree_model_get(model, a, COL_YEAR_BORN, &year1, -1);
        gtk_tree_model_get(model, b, COL_YEAR_BORN, &year2, -1);
 
        if (year1 != year2)
        {
          ret = (year1 > year2) ? 1 : -1;
        }
        /* else both equal => ret = 0 */
      }
      break;
 
      default:
        g_return_val_if_reached(0);
    }
 
    return ret;
  }
 
 
  void
  create_list_and_view (void)
  {
    GtkTreeSortable *sortable;
 
    ...
 
    liststore = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_UINT);
 
    sortable = GTK_TREE_SORTABLE(liststore);
 
    gtk_tree_sortable_set_sort_func(sortable, SORTID_NAME, sort_iter_compare_func,
                                    GINT_TO_POINTER(SORTID_NAME), NULL);
 
    gtk_tree_sortable_set_sort_func(sortable, SORTID_YEAR, sort_iter_compare_func,
                                    GINT_TO_POINTER(SORTID_YEAR), NULL);
 
    /* set initial sort order */
    gtk_tree_sortable_set_sort_column_id(sortable, SORTID_NAME, GTK_SORT_ASCENDING);
 
    ...
 
    view = gtk_tree_view_new_with_model(liststore);
 
    ...
 
  }


Usually things are a bit easier if you make use of the tree view column headers for sorting, in which case you only need to assign sort column IDs and your compare functions, but do not need to set the current sort column ID or order yourself (see below).

Your tree iter compare function should return a negative value if the row specified by iter a comes before the row specified by iter b, and a positive value if row b comes before row a. It should return 0 if both rows are equal according to your sorting criterion (you might want to use a second sort criterion though to avoid 'jumping' of equal rows when the store gets resorted). Your tree iter compare function should not take the sort order into account, but assume an ascending sort order (otherwise bad things will happen).

GtkTreeModelSortEdit

GtkTreeModelSort is a wrapper tree model. It takes another tree model such as a list store or a tree store as child model, and presents the child model to the 'outside' (i.e. a tree view or whoever else is accessing it via the tree model interface) in a sorted state. It does that without changing the order of the rows in the child model. This is useful if you want to display the same model in different tree views with different sorting criteria for each tree view, for example, or if you need to restore the original unsorted state of your store again at some point.

GtkTreeModelSort implements the GtkTreeSortable interface, so you can treat it just as if it was your data store for sorting purposes. Here is the basic setup with a tree view:

  ...
 
  void
  create_list_and_view (void)
  {
    ...
 
    liststore = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_UINT);
 
    sortmodel = gtk_tree_model_sort_new_with_model(liststore);
 
    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(sortmodel), SORTID_NAME,
                                    sort_func, GINT_TO_POINTER(SORTID_NAME), NULL);
 
    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(sortmodel), SORTID_YEAR,
                                    sort_func, GINT_TO_POINTER(SORTID_YEAR), NULL);
 
    /* set initial sort order */
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sortmodel),
                                         SORTID_NAME, GTK_SORT_ASCENDING);
 
    ...
 
    view = gtk_tree_view_new_with_model(sortmodel);
 
    ...
 
  }
 
  ...


However, when using the sort tree model, you need to be careful when you use iters and paths with the model. This is because a path pointing to a row in the view (and the sort tree model here) does probably not point to the same row in the child model which is your original list store or tree store, because the row order in the child model is probably different from the sorted order. Similarly, an iter that is valid for the sort tree model is not valid for the child model, and vice versa. You can convert paths and iters from and to the child model using gtk_tree_model_sort_convert_child_path_to_path, gtk_tree_model_sort_convert_child_iter_to_iter, gtk_tree_model_sort_convert_path_to_child_path, and gtk_tree_model_sort_convert_iter_to_child_iter. You are unlikely to need these functions frequently though, as you can still directly use gtk_tree_model_get on the sort tree model with a path supplied by the tree view.

For the tree view, the sort tree model is the 'real' model - it knows nothing about the sort tree model's child model at all, which means that any path or iter that you get passed from the tree view in a callback or otherwise will refer to the sort tree model, and that you need to pass a path or iter referring to the sort tree model as well if you call tree view functions.

Sorting and Tree View Column HeadersEdit

Unless you have hidden your tree view column headers or use custom tree view column header widgets, each tree view column's header can be made clickable. Clicking on a tree view column's header will then sort the list according to the data in that column. You need to do two things to make this happen: firstly, you need to tell your model which sort function to use for which sort column ID with gtk_tree_sortable_set_sort_func. Once you have done this, you tell each tree view column which sort column ID should be active if this column's header is clicked. This is done with gtk_tree_view_column_set_sort_column_id.

And that is really all you need to do to get your list or tree sorted. The tree view columns will automatically set the active sort column ID and sort order for you if you click on a column header.

Case-insensitive String ComparingEdit

As described above in the "GtkCellRendererText, UTF8, and pango markup" section, all strings that are to be displayed in the tree view need to be encoded in UTF8 encoding. All ASCII strings are valid UTF8, but as soon as non-ASCII characters are used, things get a bit tricky and the character encoding matters.

Comparing two ASCII strings ignoring the case is trivial and can be done using g_ascii_strcasecmp, for example. strcasecmp will usually do the same, only that it is also locale-aware to some extent. The only problem is that a lot of users use locale character encodings that are not UTF8, so strcasecmp does not take us very far.

g_utf8_collate will compare two strings in UTF8 encoding, but it does not ignore the case. In order to achieve at least half-way correct linguistic case-insensitive sorting, we need to take a two-step approach. For example, we could use g_utf8_casefold to convert the strings to compare into a form that is independent of case, and then use g_utf8_collate to compare those two strings (note that the strings returned by g_utf8_casefold will not resemble the original string in any recognisable way; they will work fine for comparisons though). Alternatively, one could use g_utf8_strdown on both strings and then compare the results again with g_utf8_collate.

Obviously, all this is not going to be very fast, and adds up if you have a lot of rows. To speed things up, you can create a 'collation key' with g_utf8_collate_key and store that in your model as well. A collation key is just a string that does not mean anything to us, but can be used with strcmp for string comparison purposes (which is a lot faster than g_utf8_collate).

It should be noted that the way g_utf8_collate sorts is dependent on the current locale. Make sure you are not working in the 'C' locale (=default, none specified) before you are wondering about weird sorting orders. Check with 'echo $LANG' on a command line what you current locale is set to.

Check out the "Unicode Manipulation" section in the GLib API Reference for more details.