For someone who wants to write adminstrative office software that communicates with a database in the background, Gtk’s TreeView
object is probably one of the most interesting widgets. Unfortunately, with its versatile features, it is also more challenging to program and there is hardly any documentation with hands-on examples. One problem I got stuck with for a while is that there are code examples for sorting functions and there are code examples for filters. But there is little Python code out there to produce a TreeView featuring both filtering and sorting the data displayed at the same time.
1. The Problem
The usual point of departure to get acquainted with TreeViews is probably the readthedocs.io turoial on TreeViews. The (complete) coding example included produces a TreeView that allows to filter a store but cannot sort the data displayed.
Now how should we amend the code in case we would like to add sortable columns on top of the language filter?
According to the documentation, a sorting feature for a column can be added by issuing a set_sort_column_id()
method call to the TreeViewColumn
object before appending it to the TreeView
:
1 2 3 4 5 |
for i, column_title in enumerate(["Software", "Release Year", "Programming Language"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) column.set_sort_column_id(i) self.treeview.append_column(column) |
The integer argument passed to the set_sort_column(int)
method call refers to the column number in the ListStore
model.
This works fine if there is no filter at the same time as can be seen in the following youtube demonstration. In case the TreeView
also includes filtering functionality, though, clicking on the column will have no visible effect. Instead, GTK’s Python wrapper will issue a couple of warnings at the Eclipse IDE Console:
1 2 3 |
gtk_tree_sortable_get_sort_column_id: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed gtk_tree_sortable_has_default_sort_func: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed gtk_tree_sortable_set_sort_column_id: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed |
I found the solution to this problem in a discussion on stackoverflow:
The idea is to stack a ListStore, a TreeModelFilter and a TreeSortFilter one inside the other and feed the last one as the model for the treeview.
2. Solution
So we will do as suggested, here’s the recipe:
- Start creating the
ListStore
using theGtk.ListStore()
constructor and append all entries. - Create a
TreeModelFilter
object connected to theListStore
using thefilter_new()
method. Link it to a custom filter function that governs the filtering process when one of the filter buttons is clicked. To that purpose, create an instance variable that is later read by the custom filter function and set by the filtering buttons. - Now comes the new part: We create an instance of the
TreeModelSort
class. We will pass theTreeModelFilter
object from the pevious step as an argument to theTreeModelSort
constructor. - We will finally create an instance of our
TreeView
class and then connect it to theTreeModelSort
object from the previous by sending theset_model()
message to theTreeView
object. - Produce
TreeViewColumn
objects to form the columns of the TreeView. Use theset_sort_column_id
to determine which column should be sortable according to which data in theListStore
.
The following sections show the detailed steps:
2.1 Create the ListStore
and populate it with data
We start with preparing the ListStore
, no big suprises here:
1 2 3 4 |
softwareListStore = Gtk.ListStore(str, int, str) for software_ref in software_list: softwareListStore.append(list(software_ref)) self.current_filter_language = None |
2.2 Create a TreeModelFilter
and prepare its connection to the filtering function
Note that the filtering function is defined later inside the same class. For the moment, it’s enough to create an instance of TreeModelFilter
and to specify the name of the function which is later used to do the filtering.
Keep in mind that the filter_new()
method returns a filter object that can be used on the softwareListStore
. So the instance variable language_filter
holds an object of TreeModelFilter
.
1 2 |
self.language_filter = softwareListStore.filter_new() self.language_filter.set_visible_func(self.language_filter_func) |
2.3 Create a TreeModelSort
object from the TreeModelFilter
object
This is going to be the additional part that really makes the difference. In the reathedocs example, we would take the TreeModelFilter
object and directly produce a TreeView
object. But instead we will now take the TreeModelFilter
object and first produce a TreeModelSort
object from it. – No sorting without a TreeModelSort
object!
1 |
self.sorted_and_filtered_model = Gtk.TreeModelSort(self.language_filter) |
Note that the sorted_and_filtered_model
instance variable holds an object of type TreeModelSort
but now also allows not only sorting but filtering because its constructor was called with a TreeModelFilter
object as an argument.
2.4 Set the TreeView
‘s underlying model to the TreeModelSort
object
While in the standard example the underlying model has been set in one shot by adding it as an argument to the TreeView
constructor call, we will do that in 2 steps now. First construct, then set the model:
1 2 |
self.treeview = Gtk.TreeView() self.treeview.set_model(self.sorted_and_filtered_model) |
2.5 Create sortable TreeViewColumn
objects for the TreeView
The final step is to create the TreeViewColumn
object that should constitute the TreeView
. The only new thing as compared to the basic example from readthedocs is that now that we have an underlying model of TreeModelSort
, we can safely use the set_sort_column_id()
method on each column object.
1 2 3 4 5 |
for i, column_title in enumerate(["Software", "Release Year", "Programming Language"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) column.set_sort_column_id(i) self.treeview.append_column(column) |
3. Complete code
Here’s the complete code for our extended example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk #list of tuples for each software, containing the software name, initial release, and main programming languages used software_list = [("Firefox", 2002, "C++"), ("Eclipse", 2004, "Java" ), ("Pitivi", 2004, "Python"), ("Netbeans", 1996, "Java"), ("Chrome", 2008, "C++"), ("Filezilla", 2001, "C++"), ("Bazaar", 2005, "Python"), ("Git", 2005, "C"), ("Linux Kernel", 1991, "C"), ("GCC", 1987, "C"), ("Frostwire", 2004, "Java"), ("Gitolite", 2007, "Perl"), ("iOS", 2008, "C++")] class TreeViewFilterWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title="TreeView Filter Demo") self.set_border_width(10) grid = Gtk.Grid() grid.set_column_homogeneous(True) # note that this enforces the window size grid.set_row_homogeneous(True) # the grid will be an 8x8 cells matrix with each cell self.add(grid) # having a height and a width equivalent to those of a # standard command button #Creating ListStore model, feeding it with the list softwareListStore = Gtk.ListStore(str, int, str) for software_ref in software_list: softwareListStore.append(list(software_ref)) self.current_filter_language = None #instance variable inspected by self.language_filter_func #and set by on_selection_button_clicked method #Creating the filter, feeding it with the liststore model self.language_filter = softwareListStore.filter_new() # this func returns a TreeModelFilter object self.language_filter.set_visible_func(self.language_filter_func) # set_func to govern the filtering process # Now that we have a Tree model filter we pack it into a TreeModelSort Object self.sorted_and_filtered_model = Gtk.TreeModelSort(self.language_filter) #Creating the TreeView, making it us the filter as model and adding the cols #self.treeview = Gtk.TreeView.new_with_model(self.language_filter) self.treeview = Gtk.TreeView() self.treeview.set_model(self.sorted_and_filtered_model) for i, column_title in enumerate(["Software", "Release Year", "Programming Language"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) column.set_sort_column_id(i) self.treeview.append_column(column) #Creating buttons to filter by Programming language and setting up their events self.buttons = list() for progLanguage in ["Java", "C++", "Python", "None"]: button = Gtk.Button(label=progLanguage) self.buttons.append(button) button.connect("clicked", self.on_selection_button_clicked) #Setting up the layout, putting the treeview in a scrollwindow and the buttons in a row scrollable_treelist = Gtk.ScrolledWindow() scrollable_treelist.set_vexpand(False) grid.attach(scrollable_treelist, 0, 0, 8, 10) grid.attach_next_to(self.buttons[0], scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1) for i, button in enumerate(self.buttons[1:]): grid.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1) scrollable_treelist.add(self.treeview) self.connect("destroy", Gtk.main_quit) self.show_all() def language_filter_func(self, model, it, data): """Tests if the language in the row is the one on the filter""" if self.current_filter_language is None or self.current_filter_language=="None": return True else: return model[it][2] == self.current_filter_language def on_selection_button_clicked(self, widget): """Called on any of the button clicks""" #we set the current filter language to the button's label self.current_filter_language = widget.get_label() print("%s language selected!" % self.current_filter_language) #we update the filter, which in turn updates the view self.language_filter.refilter() win = TreeViewFilterWindow() Gtk.main() |