Saturday, October 27, 2007

PyGTK Treeview digest

I'm always forgetting how to use the TreeView widget to display columnar data. In PyGTK (well, GTK+ in general) a TreeView is used to display trees (TreeStores) and columnar data (ListStores) like in the screenshot below. In this mini HOWTO I'm restricting the post to ListStores, however, many of the concepts below apply to TreeStores.)

A ListStore keeps track of the values you want to display in your TreeView. You create one like this:
liststore = gtk.ListStore(type1, type2, type3, ...)

Where typen can be a Python type int, str, a PyGTK type gtk.Button..., or a gobject type like gobject.TYPE_CHAR or 'gchar'.

Once your ListStore or "model" is created, you'll want to put things in it and take things out. To add a row you use append or prepend like so
iter=liststore.append([1,2,3,'a','b','c']) iter=liststore.prepend([1,2,3,'a','b','c'])
The iterator iter can be used by other calls like
new_iter=liststore.insert_before(iter, [1,2,3,'a','b','c']) new_iter=liststore.insert_after(iter, row=[...]).

So what about accessing/changing things after the fact?
value=liststore.get_value(iter, column)
will get a single column from a single row at iter.
values=liststore.get(iter,col1,col2,col3,...)
gets multiple rows from iter just as
vala, valb=liststore.get(iter, 0, 2)
does.

Conversly,
liststore.set(iter,col,val)
sets a single column in iter to value. As does
liststore.set(iter,col,val,col,val,...,...)
for multiple columns and values. To remove a single row do
liststore.remove(iter)
, and
liststore.clear()
to remove them all.

The model (ListStore) is essentially a database. To actually see the contents of it in a widget you'll need a TreeView, TreeViewColumns, and CellRenderers.
view=gtk.TreeView(model=None)
does the job of creating a TreeView, optionally setting the model off of which it's based. Otherwise it can be set and retrieved with
view.set_model(model) model=view.get_model().

TreeViewColumns hold the CellRenderers and are managed by the TreeView. You add a TreeViewColumn to a TreeView like this:
view.append_column(column).
You make one thus
col=gtk.TreeViewColumn('Heading').

CellRenderers do the work of displaying a particular column in the store. CellRenderers get packed into the TreeViewColumns (which manage the column headers), therefore you can have multiple CellRenderers under one column heading. It is a common practice to put an image and text together under the same column heading. Different Cellrenderers exist for different types namely pixmaps, text, and toggle buttons. (And, of course, you can always make your own.) It is easy to create a Cellrenderer:
cell=gtk.CellRendererText() cell=gtk.CellRendererPixbuf() cell=gtk.CellRendererToggle()
. You can then pack it into the the desired TreeViewColumn with
column.pack_start(cell,expand=True)
and
column.pack_end(cell,expand=False).

Here is the part I have the hardest time remembering... How do you associate data in the store with what gets displayed in the CellRenderers? The answer is column attributes. So, to associate the text of column 0 in the model with the text in a particular CellRenderer cell you do
col.add_attribute(cell, 'text', 0)
or equivalently
col.set_attributes(cell, text=0, prop1=modelcol1, prop2=modelcol2, ...)
. There are many other attributes besides 'text' that can be tied to your data model. Some include 'markup' (simple HTML), 'font', 'background' (color), 'foreground' (color).

Here is a minimalist example that ties it all together:

import gtk

store = gtk.ListStore(int,str,str,str)
view = gtk.TreeView(model=store) # Associate the store with the view

column_int = gtk.TreeViewColumn('Integer') # These will be our column
column_str = gtk.TreeViewColumn('String')  #  headings in the TreeView widget

view.append_column(column_int) # Associate the columns with the TreeView
view.append_column(column_str) #

cell_int = gtk.CellRendererText()
cell_hex = gtk.CellRendererText()
cell_dec = gtk.CellRendererText()

column_int.pack_start(cell_int) # This column only has one CellRenderer

column_str.pack_start(cell_hex) # This column has two
column_str.pack_start(cell_dec) #

column_int.add_attribute(cell_int, 'text', 0)       # Associate col 0 of model with text of cell_int
column_int.add_attribute(cell_int, 'background', 3) # Associate col 3 of model with the cell background

column_str.add_attribute(cell_hex, 'text', 1)       # Associate col 1 of model with text of cell_hex
column_str.add_attribute(cell_dec, 'text', 2)       #    ditto for cell_dec
column_str.add_attribute(cell_hex, 'background', 3) # Associate col 3 of model with the cell background
column_str.add_attribute(cell_dec, 'background', 3) # Associate col 3 of model with the cell background

store.append([1,'0x01','1','red'])   # Add items to the store
store.append([2,'0x02','2','green']) #
store.append([3,'0x03','3','blue'])  #
store.append([4,'0x04','4','wheat']) #
store.append([5,'0x05','5','gray'])  #
store.append([0,'','',''])           #

window = gtk.Window()
window.add(view)

window.show_all()
window.connect('delete_event', gtk.main_quit)
gtk.main()

I've written it to be as simple as possible, (hence no classes, functions, etc.), so that the associations between the ListStore, TreeView, TreeViewColumns, and CellRenderers are obvious. The result is ugly (both the code and the screenshot!), but effective at achieving the desired result. Note that the hex and dec CellRenderers are both under the 'Strings' heading and how you can associate the model with more than just the text, in this case the background color of the cell.

For more details including sorting, additional attributes, and signals see chapter 14 of the PyGTK tutorial http://www.pygtk.org/pygtk2tutorial/sec-TreeModelInterface.html upon which this post is heavily based.