{"id":1167,"date":"2018-11-05T02:06:27","date_gmt":"2018-11-05T01:06:27","guid":{"rendered":"http:\/\/hobbykeller.spdns.de\/?p=1167"},"modified":"2018-12-03T21:07:55","modified_gmt":"2018-12-03T20:07:55","slug":"pygtk-treeview-model-with-both-filter-and-sort-features","status":"publish","type":"post","link":"https:\/\/hobbykeller.spdns.de\/?p=1167","title":{"rendered":"pyGTK &#8211; TreeView model with both filter and sort features"},"content":{"rendered":"<p>For someone who wants to write adminstrative office software that communicates with a database in the background, Gtk&#8217;s <code>TreeView<\/code> 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 <a href=\"https:\/\/www.youtube.com\/watch?v=vNxhi2a2SpI&amp;index=12&amp;list=PL6gx4Cwl9DGBBnHFDEANbv9q8T4CONGZE\" target=\"_blank\" rel=\"noopener\">code examples for sorting functions<\/a> and there are <a href=\"https:\/\/python-gtk-3-tutorial.readthedocs.io\/en\/latest\/treeview.html#the-model\" target=\"_blank\" rel=\"noopener\">code examples for filters<\/a>. But there is little Python code out there to produce a TreeView featuring both filtering and sorting the data displayed at the same time.<\/p>\n<p><!--more--><\/p>\n<h1>1. The Problem<\/h1>\n<p>The usual point of departure to get acquainted with TreeViews is probably the <a href=\"https:\/\/python-gtk-3-tutorial.readthedocs.io\/en\/latest\/treeview.html#the-model\" target=\"_blank\" rel=\"noopener\">readthedocs.io turoial<\/a> on TreeViews. The (complete) coding example included produces a TreeView that allows to filter a store but cannot sort the data displayed.<\/p>\n<p><em>Now how should we amend the code in case we would like to add sortable columns on top of the language filter?<\/em><\/p>\n<p>According to the documentation, a sorting feature for a column can be added by issuing a <code>set_sort_column_id()<\/code> method call to the\u00a0<code>TreeViewColumn<\/code> object before appending it to the <code>TreeView<\/code>:<\/p>\n<pre class=\"lang:default decode:true \" title=\"Adding sorting feature to the TreeView\">\u00a0       for i, column_title in enumerate([\"Software\", \"Release Year\", \"Programming Language\"]):\r\n            renderer = Gtk.CellRendererText()\r\n            column = Gtk.TreeViewColumn(column_title, renderer, text=i)\r\n            column.set_sort_column_id(i)\r\n            self.treeview.append_column(column)<\/pre>\n<p>The integer argument passed to the <code>set_sort_column(int)<\/code> method call refers to the column number in the <code>ListStore<\/code> model.<\/p>\n<p>This works fine if there is no filter at the same time as can be seen in the following <a href=\"https:\/\/www.youtube.com\/watch?v=vNxhi2a2SpI&amp;index=12&amp;list=PL6gx4Cwl9DGBBnHFDEANbv9q8T4CONGZE\" target=\"_blank\" rel=\"noopener\">youtube demonstration<\/a>. In case the <code>TreeView<\/code> also includes filtering functionality, though, clicking on the column will have no visible effect. Instead, GTK&#8217;s Python wrapper will issue a couple of warnings at the Eclipse IDE Console:<\/p>\n<pre class=\"lang:default decode:true \" title=\"GTK Warnings with failed column sorting feature\">gtk_tree_sortable_get_sort_column_id: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed\r\ngtk_tree_sortable_has_default_sort_func: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed\r\ngtk_tree_sortable_set_sort_column_id: assertion 'GTK_IS_TREE_SORTABLE (sortable)' failed<\/pre>\n<p>I found the solution to this problem in a <a href=\"https:\/\/stackoverflow.com\/questions\/12368059\/a-sorted-and-filtered-treemodel-in-python-gtk3\" target=\"_blank\" rel=\"noopener\">discussion on stackoverflow<\/a>:<\/p>\n<blockquote><p>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.<\/p><\/blockquote>\n<h1>2. Solution<\/h1>\n<p>So we will do as suggested, here&#8217;s the recipe:<\/p>\n<ul>\n<li>Start creating the <code>ListStore<\/code> using the <code>Gtk.ListStore()<\/code> constructor and append all entries.<\/li>\n<li>Create a <code>TreeModelFilter<\/code> object connected to the <code>ListStore<\/code> using the <code>filter_new()<\/code> 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.<\/li>\n<li>Now comes the new part: We create an instance of the <code>TreeModelSort<\/code>class. We will pass the <code>TreeModelFilter<\/code> object from the pevious step as an argument to the <code>TreeModelSort<\/code>constructor.<\/li>\n<li>We will finally create an instance of our <code>TreeView<\/code> class and then connect it to the <code>TreeModelSort<\/code> object from the previous by sending the <code>set_model()<\/code> message to the <code>TreeView<\/code> object.<\/li>\n<li>Produce <code>TreeViewColumn<\/code> objects to form the columns of the TreeView. Use the <code>set_sort_column_id<\/code> to determine which column should be sortable according to which data in the <code>ListStore<\/code>.<\/li>\n<\/ul>\n<p>The following sections show the detailed steps:<\/p>\n<h2>2.1 Create the <code>ListStore<\/code> and populate it with data<\/h2>\n<p>We start with preparing the <code>ListStore<\/code>, no big suprises here:<\/p>\n<pre class=\"lang:default decode:true\" title=\"Creating the list store\">        softwareListStore = Gtk.ListStore(str, int, str)\r\n        for software_ref in software_list:\r\n            softwareListStore.append(list(software_ref))\r\n        self.current_filter_language = None<\/pre>\n<h2>2.2 Create a <code>TreeModelFilter<\/code> and prepare its connection to the filtering function<\/h2>\n<p>Note that the filtering function is defined later inside the same class. For the moment, it&#8217;s enough to create an instance of <code>TreeModelFilter<\/code> and to specify the name of the function which is later used to do the filtering.<\/p>\n<p>Keep in mind that the <code>filter_new()<\/code>method returns a filter object that can be used on the <code>softwareListStore<\/code>. So the instance variable <code>language_filter<\/code> holds an object of\u00a0 <code>TreeModelFilter<\/code>.<\/p>\n<pre class=\"lang:default decode:true\" title=\"Creating the TReeModelFilter and connecting it to the ListStore and the filtering function\">        self.language_filter = softwareListStore.filter_new()\r\n        self.language_filter.set_visible_func(self.language_filter_func)<\/pre>\n<h2>2.3 Create a <code>TreeModelSort<\/code>object from the <code>TreeModelFilter<\/code>object<\/h2>\n<p>This is going to be the additional part that really makes the difference. In the reathedocs example, we would take the <code>TreeModelFilter<\/code> object and directly produce\u00a0 a <code>TreeView<\/code> object. But instead we will now take the <code>TreeModelFilter<\/code> object and first produce a <code>TreeModelSort<\/code>object from it. &#8211; No sorting without a <code>TreeModelSort<\/code> object!<\/p>\n<pre class=\"lang:default decode:true \" title=\"Create the TreeModelSort object\">self.sorted_and_filtered_model = Gtk.TreeModelSort(self.language_filter)<\/pre>\n<p>Note that the <code>sorted_and_filtered_model<\/code> instance variable holds an object of type <code>TreeModelSort<\/code> but now also allows not only sorting but filtering because its constructor was called with a <code>TreeModelFilter<\/code> object as an argument.<\/p>\n<h2>2.4 Set the <code>TreeView<\/code>&#8216;s underlying model to the <code>TreeModelSort<\/code> object<\/h2>\n<p>While in the standard example the underlying model has been set in one shot by adding it as an argument to the <code>TreeView<\/code> constructor call, we will do that in 2 steps now. First construct, then set the model:<\/p>\n<pre class=\"lang:default decode:true \" title=\"Set the TreeModelSort object as the TreeViews underlying model\">        self.treeview = Gtk.TreeView()\r\n        self.treeview.set_model(self.sorted_and_filtered_model)<\/pre>\n<h2>2.5 Create sortable <code>TreeViewColumn<\/code> objects for the <code>TreeView<\/code><\/h2>\n<p>The final step is to create the <code>TreeViewColumn<\/code> object that should constitute the <code>TreeView<\/code>. The only new thing as compared to the basic example from readthedocs is that now that we have an underlying model of <code>TreeModelSort<\/code>, we can safely use the <code>set_sort_column_id()<\/code> method on each column object.<\/p>\n<pre class=\"lang:default decode:true \" title=\"Creating the (sortable) columns for the TreeView\">        for i, column_title in enumerate([\"Software\", \"Release Year\", \"Programming Language\"]):\r\n            renderer = Gtk.CellRendererText()\r\n            column = Gtk.TreeViewColumn(column_title, renderer, text=i)\r\n            column.set_sort_column_id(i)\r\n            self.treeview.append_column(column)<\/pre>\n<h1>3. Complete code<\/h1>\n<p>Here&#8217;s the complete code for our extended example:<\/p>\n<pre class=\"lang:default decode:true \" title=\"Complete code for sortable and filterable TreeView\">import gi\r\ngi.require_version('Gtk', '3.0')\r\nfrom gi.repository import Gtk\r\n\r\n#list of tuples for each software, containing the software name, initial release, and main programming languages used\r\nsoftware_list = [(\"Firefox\", 2002,  \"C++\"),\r\n                 (\"Eclipse\", 2004, \"Java\" ),\r\n                 (\"Pitivi\", 2004, \"Python\"),\r\n                 (\"Netbeans\", 1996, \"Java\"),\r\n                 (\"Chrome\", 2008, \"C++\"),\r\n                 (\"Filezilla\", 2001, \"C++\"),\r\n                 (\"Bazaar\", 2005, \"Python\"),\r\n                 (\"Git\", 2005, \"C\"),\r\n                 (\"Linux Kernel\", 1991, \"C\"),\r\n                 (\"GCC\", 1987, \"C\"),\r\n                 (\"Frostwire\", 2004, \"Java\"),\r\n                 (\"Gitolite\", 2007, \"Perl\"),\r\n                 (\"iOS\", 2008, \"C++\")]\r\n\r\nclass TreeViewFilterWindow(Gtk.Window):\r\n    def __init__(self):\r\n        Gtk.Window.__init__(self, title=\"TreeView Filter Demo\")\r\n        self.set_border_width(10)\r\n        \r\n        grid = Gtk.Grid()\r\n        grid.set_column_homogeneous(True) # note that this enforces the window size\r\n        grid.set_row_homogeneous(True)    # the grid will be an 8x8 cells matrix with each cell\r\n        self.add(grid)                    # having a height and a width equivalent to those of a\r\n                                          # standard command button\r\n  \r\n        #Creating ListStore model, feeding it with the list\r\n        softwareListStore = Gtk.ListStore(str, int, str)\r\n        for software_ref in software_list:\r\n            softwareListStore.append(list(software_ref))\r\n        self.current_filter_language = None                 #instance variable inspected by self.language_filter_func\r\n                                                            #and set by on_selection_button_clicked method\r\n        \r\n        #Creating the filter, feeding it with the liststore model\r\n        self.language_filter = softwareListStore.filter_new()          # this func returns a TreeModelFilter object\r\n        self.language_filter.set_visible_func(self.language_filter_func) # set_func to govern the filtering process\r\n        \r\n        # Now that we have a Tree model filter we pack it into a TreeModelSort Object\r\n        self.sorted_and_filtered_model = Gtk.TreeModelSort(self.language_filter)\r\n        \r\n        #Creating the TreeView, making it us the filter as model and adding the cols\r\n        #self.treeview = Gtk.TreeView.new_with_model(self.language_filter)\r\n        self.treeview = Gtk.TreeView()\r\n        self.treeview.set_model(self.sorted_and_filtered_model)\r\n        \r\n        for i, column_title in enumerate([\"Software\", \"Release Year\", \"Programming Language\"]):\r\n            renderer = Gtk.CellRendererText()\r\n            column = Gtk.TreeViewColumn(column_title, renderer, text=i)\r\n            column.set_sort_column_id(i)\r\n            self.treeview.append_column(column)\r\n        \r\n        #Creating buttons to filter by Programming language and setting up their events\r\n        self.buttons = list()\r\n        for progLanguage in [\"Java\", \"C++\", \"Python\", \"None\"]:\r\n            button = Gtk.Button(label=progLanguage)\r\n            self.buttons.append(button)\r\n            button.connect(\"clicked\", self.on_selection_button_clicked)\r\n            \r\n        #Setting up the layout, putting the treeview in a scrollwindow and the buttons in a row\r\n        scrollable_treelist = Gtk.ScrolledWindow()\r\n        scrollable_treelist.set_vexpand(False)\r\n        grid.attach(scrollable_treelist, 0, 0, 8, 10)\r\n        grid.attach_next_to(self.buttons[0], scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1)\r\n        for i, button in enumerate(self.buttons[1:]):\r\n            grid.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)\r\n        scrollable_treelist.add(self.treeview)\r\n        \r\n        self.connect(\"destroy\", Gtk.main_quit)\r\n        self.show_all()\r\n        \r\n    \r\n    def language_filter_func(self, model, it, data):\r\n        \"\"\"Tests if the language in the row is the one on the filter\"\"\"\r\n        if self.current_filter_language is None or self.current_filter_language==\"None\":\r\n            return True\r\n        else:\r\n            return model[it][2] == self.current_filter_language\r\n        \r\n    def on_selection_button_clicked(self, widget):\r\n        \"\"\"Called on any of the button clicks\"\"\"\r\n        #we set the current filter language to the button's label\r\n        self.current_filter_language = widget.get_label()\r\n        print(\"%s language selected!\" % self.current_filter_language)\r\n        #we update the filter, which in turn updates the view\r\n        self.language_filter.refilter()\r\n  \r\nwin = TreeViewFilterWindow()\r\nGtk.main()<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>For someone who wants to write adminstrative office software that communicates with a database in the background, Gtk&#8217;s TreeView object is probably one of the most interesting widgets. Unfortunately, with<span class=\"more-button\"><a href=\"https:\/\/hobbykeller.spdns.de\/?p=1167\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\">pyGTK &#8211; TreeView model with both filter and sort features<\/span><\/a><\/span><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[255,258,64,254],"tags":[263,251,250,132,262,265,264,261,266],"class_list":["post-1167","post","type-post","status-publish","format-standard","hentry","category-gtk","category-gui","category-linux","category-python","tag-filtering","tag-gtk","tag-pygtk","tag-python","tag-sorting","tag-treemodelfilter","tag-treemodesort","tag-treeview","tag-treeviewcolumn"],"_links":{"self":[{"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/posts\/1167","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1167"}],"version-history":[{"count":4,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/posts\/1167\/revisions"}],"predecessor-version":[{"id":1173,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=\/wp\/v2\/posts\/1167\/revisions\/1173"}],"wp:attachment":[{"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1167"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1167"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hobbykeller.spdns.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1167"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}