14.2. The TreeModel Interface and Data Stores

14.2.1. Introduction

The TreeModel interface is implemented by all the TreeModel subclasses and provides methods to:

  • retrieve the characteristics of the data store such as the number of columns and the type of data in a column.
  • retrieve a TreeIter (a transient reference) that points at a row in the model
  • retrieve information about a node (or row) such as the number of its child nodes, a list of its child nodes, the contents of its columns and a pointer to its parent node
  • provide notification of TreeModel data changes

14.2.2. Creating TreeStore and ListStore Objects

The base data store classes: ListStore and TreeStore provide the means to define and manage the rows and columns of data in the tree model. The constructors of both these objects require the column types to be specified as any of:

  • Python types such as the built-in types: int, str, long, float and object
  • PyGTK types such as Button, VBox, gdk.Rectangle, gdk.Pixbuf
  • GObject types (GTK+ GTypes) specified either as GObject Type constants or as strings. Most GTypes are mapped to a Python type:

    • gobject.TYPE_CHAR or 'gchar'
    • gobject.TYPE_UCHAR or 'guchar'
    • gobject.TYPE_BOOLEAN or 'gboolean'
    • gobject.TYPE_INT or 'gint'
    • gobject.TYPE_UINT or 'guint'
    • gobject.TYPE_LONG or 'glong
    • gobject.TYPE_ULONG or 'gulong
    • gobject.TYPE_INT64 or 'gint64'
    • gobject.TYPE_UINT64 or 'guint64'
    • gobject.TYPE_FLOAT or 'gfloat'
    • gobject.TYPE_DOUBLE or 'gdouble'
    • gobject.TYPE_STRING or 'gchararray'
    • gobject.TYPE_OBJECT or 'GObject

For example to create a ListStore or TreeStore with rows containing a gdk.Pixbuf, an integer, a string and boolean you could do something like:

  liststore = ListStore(gtk.gdk.Pixbuf, int, str, 'gboolean')

  treestore = TreeStore(gtk.gdk.Pixbuf, int, str, 'gboolean')

Once a ListStore or TreeStore is created and its columns defined, they cannot be changed or modified. It's also important to realize that there is no preset relation between the columns in a TreeView and the columns of its TreeModel. That is, the fifth column of data in a TreeModel may be displayed in the first column of one TreeView and in the third column in another. So you don't have to worry about how the data will be displayed when creating the data store.

If these two data stores do not fit your application it is possible to define your own custom data store in Python as long as it implements the TreeModel interface. I'll talk more about this later in Section 14.11, “The Generic TreeModel”.

14.2.3. Referring to TreeModel Rows

Before we can talk about managing the data rows in a TreeStore or ListStore we need a way of specifying which row we want to deal with. PyGTK has three ways of referring to TreeModel rows: a tree path, a TreeIter and a TreeRowReference.

14.2.3.1. Tree Paths

A tree path is a int, string or tuple representation of the location of a row in the store. An int value specifies the top level row in the model starting from 0. For example, a tree path value of 4 would specify the fifth row in the store. By comparison, a string representation of the same row would be "4" and the tuple representation would be (4,). This is sufficient for specifying any row in a ListStore but for a TreeStore we have to be able to represent the child rows. For these cases we have to use either the string or tuple representations.

Since a TreeStore can have an arbitrarily deep hierarchy the string representation specifies the path from the top level to the designated row using ints separated by the ":" character. Similarly, the tuple representation specifies the tree path starting from the top level to the row as a sequence of ints. For example, valid tree path string representations are: "0:2" (specifies the row that is the third child of the first row) and "4:0:1" (specifies the row that is the second child of the first child of the fifth row). By comparison the same tree paths are represented by the tuples (0, 2) and (4, 0, 1) respectively.

A tree path provides the only way to map from a TreeView row to a TreeModel row because the tree path of a TreeView row is the same as the tree path of the corresponding TreeModel row. There are also some problems with tree paths:

  • a tree path can specify a row that doesn't exist in the ListStore or TreeStore.
  • a tree path can point to a different data row after inserting or deleting a row in the ListStore or TreeStore.

PyGTK uses the tuple representation when returning tree paths but will accept any of the three forms for a tree path representation. You should use the tuple representation for a tree path for consistency.

A tree path can be retrieved from a TreeIter using the get_path() method:

  path = store.get_path(iter)

where iter is a TreeIter pointing at a row in store and path is the row's tree path as a tuple.

14.2.3.2. TreeIters

A TreeIter is an object that provides a transient reference to a ListStore or TreeStore row. If the contents of the store change (usually because a row is added or deleted) the TreeIters can become invalid. A TreeModel that supports persistent TreeIters should set the gtk.TREE_MODEL_ITERS_PERSIST flag. An application can check for this flag using the get_flags() method.

A TreeIter is created by one of the TreeModel methods that are applicable to both TreeStore and ListStore objects:

  treeiter = store.get_iter(path)

where treeiter points at the row at the tree path path. The ValueError exception is raised if the tree path is invalid.

  treeiter = store.get_iter_first()

where treeiter is a TreeIter pointing at the row at tree path (0,). treeiter will be None if the store is empty.

  treeiter = store.iter_next(iter)

where treeiter is a TreeIter that points at the next row at the same level as the TreeIter specified by iter. treeiter will be None if there is no next row (iter is also invalidated).

The following methods are useful only for retrieving a TreeIter from a TreeStore:

  treeiter = treestore.iter_children(parent)

where treeiter is a TreeIter pointing at the first child row of the row specified by the TreeIter parent. treeiter will be None if there is no child.

  treeiter = treestore.iter_nth_child(parent, n)

where treeiter is a TreeIter pointing at the child row (with the index n) of the row specified by the TreeIter parent. parent may be None to retrieve a top level row. treeiter will be None if there is no child.

  treeiter = treestore.iter_parent(child)

where treeiter is a TreeIter pointing at the parent row of the row specified by the TreeIter child. treeiter will be None if there is no child.

A tree path can be retrieved from a TreeIter using the get_path() method:

  path = store.get_path(iter)

where iter is a Treeiter pointing at a row in store and path is the row's tree path as a tuple.

14.2.3.3. TreeRowReferences

A TreeRowReference is a persistent reference to a row of data in a store. While the tree path (i.e. the location) of the row might change as rows are added to or deleted from the store, the TreeRowReference will point at the same data row as long as it exists.

Note

TreeRowReferences are only available in PyGTK 2.4 and above.

You can create a TreeRowReference using its constructor:

  treerowref = TreeRowReference(model, path)

where model is the TreeModel containing the row and path is the tree path of the row to track. If path isn't a valid tree path for model, None is returned.

14.2.4. Adding Rows

14.2.4.1. Adding Rows to a ListStore

Once you have a ListStore you'll need to add data rows using one of the following methods:

  iter = append(row=None)
  iter = prepend(row=None)
  iter = insert(position, row=None)
  iter = insert_before(sibling, row=None)
  iter = insert_after(sibling, row=None)

Each of these methods inserts a row at an implied or specified position in the ListStore. The append() and prepend() methods use implied positions: after the last row and before the first row, respectively. The insert() method takes an integer (the parameter position) that specifies the location where the row will be inserted. The other two methods take a TreeIter (sibling) that references a row in the ListStore to insert the row before or after.

The row parameter specifies the data that should be inserted in the row after it is created. If row is None or not specified, an empty row will be created. If row is specified it must be a tuple or list containing as many items as the number of columns in the ListStore. The items must also match the data type of their respective ListStore columns.

All methods return a TreeIter that points at the newly inserted row. The following code fragment illustrates the creation of a ListStore and the addition of data rows to it:

  ...
  liststore = gtk.ListStore(int, str, gtk.gdk.Color)
  liststore.append([0,'red',colormap.alloc_color('red')])
  liststore.append([1,'green',colormap.alloc_color('green')])
  iter = liststore.insert(1, (2,'blue',colormap.alloc_color('blue')) )
  iter = liststore.insert_after(iter, [3,'yellow',colormap.alloc_color('blue')])
  ...

14.2.4.2. Adding Rows to a TreeStore

Adding a row to a TreeStore is similar to adding a row to a ListStore except that you also have to specify a parent row (using a TreeIter) to add the new row to. The TreeStore methods are:

  iter = append(parent, row=None)
  iter = prepend(parent, row=None)
  iter = insert(parent, position, row=None)
  iter = insert_before(parent, sibling, row=None)
  iter = insert_after(parent, sibling, row=None)

If parent is None, the row will be added to the top level rows.

Each of these methods inserts a row at an implied or specified position in the TreeStore. The append() and prepend() methods use implied positions: after the last child row and before the first child row, respectively. The insert() method takes an integer (the parameter position) that specifies the location where the child row will be inserted. The other two methods take a TreeIter (sibling) that references a child row in the TreeStore to insert the row before or after.

The row parameter specifies the data that should be inserted in the row after it is created. If row is None or not specified, an empty row will be created. If row is specified it must be a tuple or list containing as many items as the number of columns in the TreeStore. The items must also match the data type of their respective TreeStore columns.

All methods return a TreeIter that points at the newly inserted row. The following code fragment illustrates the creation of a TreeStore and the addition of data rows to it:

  ...
  folderpb = gtk.gdk.pixbuf_from_file('folder.xpm')
  filepb = gtk.gdk.pixbuf_from_file('file.xpm')
  treestore = gtk.TreeStore(int, str, gtk.gdk.Pixbuf)
  iter0 = treestore.append(None, [1,'(0,)',folderpb] )
  treestore.insert(iter0, 0, [11,'(0,0)',filepb])
  treestore.append(iter0, [12,'(0,1)',filepb])
  iter1 = treestore.insert_after(None, iter0, [2,'(1,)',folderpb])
  treestore.insert(iter1, 0, [22,'(1,1)',filepb])
  treestore.prepend(iter1, [21,'(1,0)',filepb])
  ...

14.2.4.3. Large Data Stores

When a ListStore or TreeStore contains a large number of data rows, adding new rows can become very slow. There are a few things that you can do to mitigate this problem:

  • If adding a large number of rows disconnect the TreeModel from its TreeView (using the set_model() method with the model parameter set to None) to avoid TreeView updates for each row entered.
  • Likewise, disable sorting (using the set_default_sort_func() method with the sort_func set to None) while adding a large number of rows.
  • Limit the number of TreeRowReferences in use since they update their path with each addition or removal.
  • Set the TreeView "fixed-height-mode" property to TRUE making all rows have the same height and avoiding the individual calculation of the height of each row. Only available in PyGTK 2.4 and above.

14.2.5. Removing Rows

14.2.5.1. Removing Rows From a ListStore

You can remove a data row from a ListStore by using the remove() method:

  treeiter = liststore.remove(iter)

where iter is a TreeIter pointing at the row to remove. The returned TreeIter (treeiter) points at the next row or is invalid if iter was pointing at the last row.

The clear() method removes all rows from the ListStore:

  liststore.clear()

14.2.5.2. Removing Rows From a TreeStore

The methods for removing data rows from a TreeStore are similar to the ListStore methods:

  result = treestore.remove(iter)
  treestore.clear()

where result is TRUE if the row was removed and iter points at the next valid row. Otherwise, result is FALSE and iter is invalidated.

14.2.6. Managing Row Data

14.2.6.1. Setting and Retrieving Data Values

The methods for accessing the data values in a ListStore and TreeStore have the same format. All store data manipulations use a TreeIter to specify the row that you are working with. Once you have a TreeIter it can be used to retrieve the values of a row column using the get_value() method:

  value = store.get_value(iter, column)

where iter is a TreeIter pointing at a row, column is a column number in store, and, value is the value stored at the row-column location.

If you want to retrieve the values from multiple columns in one call use the get() method:

  values = store.get(iter, column, ...)

where iter is a TreeIter pointing at a row, column is a column number in store, and, ... represents zero or more additional column numbers and values is a tuple containing the retrieved data values. For example to retrieve the values in columns 0 and 2:

  val0, val2 = store.get(iter, 0, 2)

Note

The get() method is only available in PyGTK 2.4 and above.

Setting a single column value is effected using the set_value() method:

  store.set_value(iter, column, value)

where iter (a TreeIter) and column (an int) specify the row-column location in store and column is the column number where value is to be set. value must be the same data type as the store column.

If you wish to set the value of more than one column in a row at a time, use the set() method:

  store.set(iter, ...)

where iter specifies the store row and ... is one or more column number - value pairs indicating the column and and value to set. For example, the following call:

  store.set(iter, 0, 'Foo', 5, 'Bar', 1, 123)

sets the first column to 'Foo', the sixth column to 'Bar' and the second column to 123 in the store row specified by iter.

14.2.6.2. Rearranging ListStore Rows

Individual ListStore rows can be moved using one of the following methods that are available in PyGTK 2.2 and above:

  liststore.swap(a, b)
  liststore.move_after(iter, position)
  liststore.move_before(iter, position)

swap() swaps the locations of the rows referenced by the TreeIters a and b. move_after() and move_before() move the row referenced by the TreeIter iter after or before the row referenced by the TreeIter position. If position is None, move_after() will place the row at the beginning of the store while move_before(), at the end of the store.

If you want to completely rearrange the ListStore data rows, use the following method:

  liststore.reorder(new_order)

where new_order is a list of integers that specify the new row order as:

  new_order[newpos] = oldpos

For example, if liststore contained four rows:

  'one'
  'two'
  'three'
  'four'

The method call:

  liststore.reorder([2, 1, 3, 0])

would produce the resulting order:

  'three'
  'two'
  'four'
  'one'

Note

These methods will only rearrange unsorted ListStores.

If you want to rearrange rows in PyGTK 2.0 you have to remove and insert rows using the methods described in Section 14.2.4, “Adding Rows” and Section 14.2.5, “Removing Rows”.

14.2.6.3. Rearranging TreeStore Rows

The methods used to rearrange TreeStore rows are similar to the ListStore methods except they only affect the child rows of an implied parent row - it is not possible to, say, swap rows with different parent rows.:

  treestore.swap(a, b)
  treestore.move_after(iter, position)
  treestore.move_before(iter, position)

swap() swaps the locations of the child rows referenced by the TreeIters a and b. a and b must both have the same parent row. move_after() and move_before() move the row referenced by the TreeIter iter after or before the row referenced by the TreeIter position. iter and position must both have the same parent row. If position is None, move_after() will place the row at the beginning of the store while move_before(), at the end of the store.

The reorder() method requires an additional parameter specifying the parent row whose child rows will be reordered:

  treestore.reorder(parent, new_order)

where new_order is a list of integers that specify the new child row order of the parent row specified by the TreeIter parent as:

  new_order[newpos] = oldpos

For example, if treestore contained four rows:

  'parent'
      'one'
      'two'
      'three'
      'four'

The method call:

  treestore.reorder(parent, [2, 1, 3, 0])

would produce the resulting order:

  'parent'
      'three'
      'two'
      'four'
      'one'

Note

These methods will only rearrange unsorted TreeStores.

14.2.6.4. Managing Multiple Rows

One of the trickier aspects of dealing with ListStores and TreeStores is the operation on multiple rows, e.g. moving multiple rows, say, from one parent row to another or removing rows based on certain criteria. The difficulty arises from the need to use a TreeIter that may become invalid as the result of the operation. For ListStores and TreeStores the TreeIters are persistent as can be checked by using the get_flags() method and testing for the gtk.TREE_MODEL_ITERS_PERSIST flag. However the stackable TreeModelFilter and TreeModelSort classes do not have persistent TreeIters.

Assuming that TreeIters don't persist how do we move all the child rows from one parent row to another? We have to:

  • iterate over the parent's children
  • retrieve each row's data
  • remove each child row
  • insert a new row with the old row data in the new parent's list

We can't rely on the remove() method to return a valid TreeIter so we'll just ask for the first child iter until it returns None. A possible function to move child rows is:

  def move_child_rows(treestore, from_parent, to_parent):
    n_columns = treestore.get_n_columns()
    iter = treestore.iter_children(from_parent)
    while iter:
      values = treestore.get(iter, *range(n_columns))
      treestore.remove(iter)
      treestore.append(to_parent, values)
      iter = treestore.iter_children(from_parent)
    return

The above function covers the simple case of moving all child rows of a single parent row but what if you want to remove all rows in the TreeStore based on some match criteria, say the first column value? Here you might think that you could use the foreach() method to iterate over all the rows and remove the matching ones:

  store.foreach(func, user_data)

where func is a function that is invoked for each store row and has the signature:

  def func(model, path, iter, user_data):

where model is the TreeModel data store, path is the tree path of a row in model, iter is a TreeIter pointing at path and user_data is the passed in data. if func returns TRUE the foreach() method will cease iterating and return.

The problem with that is that changing the contents of the store while the foreach() method is iterating over it may have unpredictable results. Using the foreach() method to create and save TreeRowReferences to the rows to be removed and then removing them after the foreach() method completes would be a good strategy except that it doesn't work for PyGTK 2.0 and 2.2 where TreeRowReferences are not available.

A reliable strategy that covers all the PyGTK variants is to use the foreach() method to gather the tree paths of rows to be removed and then remove them in reverse order to preserve the validity of the tree paths. An example code fragment utilizing this strategy is:

  ...
  # match if the value in the first column is >= the passed in value
  # data is a tuple containing the match value and a list to save paths
  def match_value_cb(model, path, iter, data):
    if model.get_value(iter, 0) >= data[0]:
      data[1].append(path)
    return False     # keep the foreach going

  pathlist = []
  treestore.foreach(match_value_cb, (10, pathlist))

  # foreach works in a depth first fashion
  pathlist.reverse()
  for path in pathlist:
    treestore.remove(treestore.get_iter(path))
  ...

If you want to search a TreeStore for the first row that matches some criteria, you probably want to do the iteration yourself using something like:

   treestore = TreeStore(str)
   ...
   def match_func(model, iter, data):
       column, key = data # data is a tuple containing column number, key
       value = model.get_value(iter, column)
       return value == key
   def search(model, iter, func, data):
       while iter:
           if func(model, iter, data):
               return iter
           result = search(model, model.iter_children(iter), func, data)
           if result: return result
           iter = model.iter_next(iter)
       return None
   ...
   match_iter = search(treestore, treestore.iter_children(None), 
                       match_func, (0, 'foo'))

The search() function iterates recursively over the row (specified by iter) and its siblings and their child rows in a depth first fashion looking for a row that has a column matching the given key string. The search terminates when a row is found.

14.2.7. Python Protocol Support

The classes that implement the TreeModel interface (TreeStore and ListStore and in PyGTK 2.4, also the TreeModelSort and TreeModelFilter) support the Python mapping and iterator protocols. The iterator protocol allows you to use the Python iter() function on a TreeModel to create an iterator to be used to iterate over the top level rows in the TreeModel. A more useful capability is to iterate using the for statement or a list comprehension. For example:

  ...
  liststore = gtk.ListStore(str, str)
  ...
  # add some rows to liststore
  ...
  # for looping
  for row in liststore:
      # do individual row processing
  ...
  # list comprehension returning a list of values in the first column
  values = [ r[0] for r in liststore ]
  ...

Other parts of the mapping protocols that are supported are using del to delete a row in the model and extracting a PyGTK TreeModelRow from the model using a key value that is a tree path or TreeIter. For example, the following statements all return the first row in a TreeModel and the final statement deletes the first child row of the first row:

  row = model[0]
  row = model['0']
  row = model["0"]
  row = model[(0,)]
  i = model.get_iter(0)
  row = model[i]
  del model[(0,0)]

In addition, you can set the values in an existing row similar to the following:

  ...
  liststore = gtk.ListStore(str, int, object)
  ...
  liststore[0] = ['Button', 23, gtk.Button('Label')]

A PyGTK TreeModelRow object supports the Python sequence and iterator protocols. You can get an iterator to iterate over the column values in the row or use the for statement or list comprehension as well. A TreeModelRow uses the column number as the index to extract a value. For example:

  ...
  liststore = gtk.ListStore(str, int)
  liststore.append(['Random string', 514])
  ...
  row = liststore[0]
  value1 = row[1]
  value0 = liststore['0'][0]
  for value in row:
      print value
  val0, val1 = row
  ...

Using the example from the previous section to iterate over a TreeStore to locate a row containing a particular value, the code becomes:

   treestore = TreeStore(str)
   ...
   def match_func(row, data):
       column, key = data # data is a tuple containing column number, key
       return row[column] == key
   ...
   def search(rows, func, data):
       if not rows: return None
       for row in rows:
           if func(row, data):
               return row
           result = search(row.iterchildren(), func, data)
           if result: return result
       return None
   ...
   match_row = search(treestore, match_func, (0, 'foo'))

You can also set a value in an existing column using:

  treestore[(1,0,1)][1] = 'abc'

The TreeModelRow also supports the del statement and conversion to lists and tuples using the Python list() and tuple() functions. As illustrated in the above example the TreeModelRow has the iterchildren() method that returns an iterator for iterating over the child rows of the TreeModelRow.

14.2.8. TreeModel Signals

Your application can track changes in a TreeModel by connecting to the signals that are emitted by the TreeModel: "row-changed", "row-deleted", "row-inserted", "row-has-child-toggled" and "rows-reordered". These signals are used by a TreeView to track changes in its TreeModel.

If you connect to these signals in your application, you may see clusters of signals when some methods are called. For example the call to add the first child row to a parent row:

  treestore.append(parent, ['qwe', 'asd', 123])

will cause the following signal emissions:

  • "row-inserted" where the inserted row will be empty.
  • "row-has-child-toggled" since parent didn't previously have any child rows.
  • "row-changed" for the inserted row when setting the value 'qwe' in the first column.
  • "row-changed" for the inserted row when setting the value 'asd in the second column.
  • "row-changed" for the inserted row when setting the value 123 in the third column.

Note that you can't retrieve the row order in the "rows-reordered" callback since the new row order is passed as an opaque pointer to an array of integers.

See the PyGTK Reference Manual for more information on the TreeModel signals.

14.2.9. Sorting TreeModel Rows

14.2.9.1. The TreeSortable Interface

The ListStore and TreeStore objects implement the TreeSortable interface that provides methods for controlling the sorting of TreeModel rows. The key element of the interface is a "sort column ID" which is an arbitrary integer value referring to a sort comparison function and associated user data. A sort column ID must be greater than or equal to zero. A sort column ID is created by using the method:

  treesortable.set_sort_func(sort_column_id, sort_func, user_data=None)

where sort_column_id is a programmer assigned integer value, sort_func is a function or method used to compare rows and user_data is context data. sort_func has the signature:

  def sort_func_function(model, iter1, iter2, data)
  def sort_func_method(self, model, iter1, iter2, data)

where model is the TreeModel containing the rows pointed to by the TreeIters iter1 and iter2 and data is user_data. sort_func should return: -1 if the iter1 row should precede the iter2 row; 0, if the rows are equal; and, 1 if the iter2 row should precede the iter1 row. The sort comparison function should always assume that the sort order is gtk.SORT_ASCENDING as the sort order will be taken into account by the TreeSortable implementations.

The same sort comparison function can be used for multiple sort column IDs by varying the user_data to provide context information. For example, the user_data specified in the set_sort_func() method could be the index of the column to extract the sort data from.

Once a sort column ID is created a store can use it for sorting by calling the method:

  treesortable.set_sort_column_id(sort_column_id, order)

where order is the sort order either gtk.SORT_ASCENDING or gtk.SORT_DESCENDING.

The sort column ID of -1 means that the store should use the default sort function that is set using the method:

  treesortable.set_default_sort_func(sort_func, user_data=None)

You can check if a store has a default sort function using the method:

  result = treesortable.has_default_sort_func()

which returns TRUE if a default sort function has been set.

Once a sort column ID has been set on a TreeModel implementing the TreeSortable interface it cannot be returned to the original unsorted state. You can change the sort function or use a default sort function but you cannot set the TreeModel to have no sort function.

14.2.9.2. Sorting in ListStores and TreeStores

When a ListStore or TreeStore object is created it automatically sets up sort column IDs corresponding to the columns in the store using the column index number. For example, a ListStore with three columns would have three sort column IDs (0, 1, 2) setup automatically. These sort column IDs are associated with an internal sort comparison function that handles the fundamental types:

  • 'gboolean'
  • str
  • int
  • long
  • float

Initially a ListStore or TreeStore is set with a sort column ID of -2 that indicates that no sort function is being used and that the store is unsorted. Once you set a sort column ID on a ListStore or TreeStore you cannot set it back to -2.

If you want to maintain the default sort column IDs you can set up a sort column ID well out of the range of the number of columns such as 1000 and up. Then you can switch between the default sort function and your application sort functions as needed.