Create  Edit  FrontPage  Index  Search  Changes  History  RSS  Login

tut-gtk2-treev-trees

Using Gtk::TreeStore

Beside Gtk::ListStore, there is one more type of built-in model used with tree view called Gtk::TreeStore which provides the same functionality as Gtk::ListStore, except the data can be organized into a multilayered tree. Despite my intentions to avoid being repetitive, I will repeat the rest of this paragraph: ""Both Gtk::ListStore and Gtk::TreeStore implement the Gtk::TreeModel interface, and inherit all of its methods. They also implement the Gtk::TreeSortable interface so you can sort the list using the view. These models provide the data structure as well as all appropriate tree interfaces. As a result, implementing drag and drop, sorting, and storing data is trivial.""

treev-trees-ok.png

When exploring the design features and elements of a list and tree store, where it should be noted that "list store" is merely a cut down or a limited version of "tree store", we encounter many additional design features, that need to be considered and explained in parallel for all models. In addition to commonly known items like columns and renderers, there are also things like Gtk::TreeIter, Gtk::TreePath and Gtk::TreeRowReference, the first of which, namely the tree iterator, we have already introduced on previous pages. Couple this with Gtk::TreeView and the amount of new material grows even more. One additional item we still need to add to our list of things to learn is the set_cell_data_func helper function. Though I originally had a segment of its own planed for it later, I believe it is best if we look at it here and explain it sufficiently, so you will be able to understand other programs utilizing it. But before we look at the 'set_cell_data_func' lets look at a revised "Grocery List" example program from previous page here appropriately called with prefix "tree" rather than "list", i.e. "treestore.rb", to quickly point out how building a list store differs from building a tree store. This program splits the products into two categories: "Cleaning Supplies" and "Food", which both have children elements or rows. The "Count" or quantity of each category is set initially to zero, and is calculated (counting all children) which have the "Buy" column set to TRUE, during the run-time hence displaying 4 for "Cleaning Supplies" and 7 for "Food" categories respectively, which incidentally is the number of their respective children with 'Buy' column set to TRUE. Note, there are two logical columns under the header Buy. The left logical column represents the parent column with a small expander triangle at the edge, slightly shifted to the right is the row of children.


treestore.rb

#!/usr/bin/env ruby
require 'gtk2'

# Add three columns to the GtkTreeView. All three of the
# columns will be displayed as text, although one is a boolean
# value and another is an integer.
def setup_tree_view(treeview)
  # Create a new GtkCellRendererText, add it to the tree
  # view column and append the column to the tree view.
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Buy", renderer, "text" => BUY_INDEX)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Count", renderer, "text" => QTY_INDEX)
  treeview.append_column(column) 
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Product", renderer, "text" => PROD_INDEX)
  treeview.append_column(column)
end

window = Gtk::Window.new("New Grocery List (tree)")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(275, 200)

class GroceryItem
  attr_accessor :product_type, :buy, :quantity, :product
  def initialize(t,b,q,p)
    @product_type, @buy, @quantity, @product = t, b, q, p
  end
end
BUY_INDEX = 0; QTY_INDEX = 1; PROD_INDEX = 2
P_CATEGORY = 0; P_CHILD = 1

list = Array.new
list[0] = GroceryItem.new(P_CATEGORY, true,  0, "Cleaning Supplies")
list[1] = GroceryItem.new(P_CHILD,    true,  1, "Paper Towels")
list[2] = GroceryItem.new(P_CHILD,    true,  3, "Toilet Paper")
list[3] = GroceryItem.new(P_CATEGORY, true,  0, "Food")
list[4] = GroceryItem.new(P_CHILD,    true,  2, "Bread")
list[5] = GroceryItem.new(P_CHILD,    false, 1, "Butter")
list[6] = GroceryItem.new(P_CHILD,    true,  1, "Milk")
list[7] = GroceryItem.new(P_CHILD,    false, 3, "Chips")
list[8] = GroceryItem.new(P_CHILD,    true,  4, "Soda")

treeview = Gtk::TreeView.new
setup_tree_view(treeview)

# Create a new tree model with three columns, as Boolean, 
# integer and string.
store = Gtk::TreeStore.new(TrueClass, Integer, String)

# Avoid creation of iterators on every iterration, since they
# need to provide state information for all iterations. Hence:
# establish closure variables for iterators parent and child.
parent = child = nil

# Add all of the products to the GtkTreeStore.
list.each_with_index do |e, i|

  # If the product type is a category, count the quantity
  # of all of the products in the category that are going
  # to be bought.
  if (e.product_type == P_CATEGORY)
    j = i + 1

    # Calculate how many products will be bought in
    # the category.
    while j < list.size && list[j].product_type != P_CATEGORY
      list[i].quantity += list[j].quantity if list[j].buy
      j += 1
    end

    # Add the category as a new root (parent) row (element).
    parent = store.append(nil)
    # store.set_value(parent, BUY_INDEX, list[i].buy) # <= same as below
    parent[BUY_INDEX]  = list[i].buy
    parent[QTY_INDEX]  = list[i].quantity
    parent[PROD_INDEX] = list[i].product

  # Otherwise, add the product as a child row of the category.
  else
    child = store.append(parent)
    # store.set_value(child, BUY_INDEX, list[i].buy) # <= same as below
    child[BUY_INDEX]  = list[i].buy
    child[QTY_INDEX]  = list[i].quantity
    child[PROD_INDEX] = list[i].product
  end
end

# Add the tree model to the tree view
treeview.model = store
scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
window.add(scrolled_win)
window.show_all
Gtk.main

With the new version of our example program we obtained slightly different presentation, namely the view, with the contents of the first view column in parent nodes sitting next to the "expander arrow" adjusted to the left of that column, and the content of this very same (first) view column for children rows indented to the right side of the column, however, without the "expander arrow", so user can discern between the two kinds of rows. Unlike the view, however, the model, with the exception of new parent child division, did not change.

Note:

The tree view will always use the first view column as the parent/child discriminating column, i.e. should you decide that the "Product" should be the first column in the view, it would be adorned with the "expander arrow" as well as with the content indentation schema, described above, to present the differentiation between parent and child rows.

You define the order of columns in the view by the order in which the Gtk::TreeView#append_column statements are executed. For instance regardless of what is the order of columns in the model, to achieve the order of our view columns as:"Product, Count, Buy"all you have to do is append the appropriate Gtk::TreeViewColumn objects to the tree view in the corresponding order:

treeview.append_column(product_column)
treeview.append_column(count_column)
treeview.append_column(buy_column)

The only difference between the original "liststore.rb" listing and the modified "treestore.rb" version of our Gtk::TreeView example programs where the first employs the Gtk::ListStore and the second the Gtk::TreeStore is in the creation but not in the structure of the models i.e.stores,hence the tree model here keeps the same structure as the list model in the original program, with three columns for which the Boolean, Integer and String data types are defined. What changed is the initialization process as well as the structure of GroceryItem class, which now includes a controlling field (attribute) called @product_type, to store a flag determining the parent/child relationship, and which subsequently is used in the model (store) initialization loop to drive either parent or child nodes creation and initialization. This, by the way, is the only thing that reflects the parent child division in the model, namely parent rows are created with nil argument (Gtk::TreeStore#append(parent=nil)), whereas children rows have this argument set to their respective parents. Incidentally, this is also different in the two source code listings, namely Gtk::ListStore#append comes with no argument. Additionally in our "treestore.rb" example, the total numbers of items to be bought in either category is calculated in separate sub-loop for each category. Note, that this initialization processing arrangement requires that all categories are bundled together in the initialization array of GroceryItem objects. This requirement is forced both by the creation of groups of children underneath the parent nodes, as well as by the way total number of products to purchase is handled.

Adding columns and renderers in our tree view creation method "setup_tree_view" is performed in the same manner with both list and tree models, because columns are part of the view not the model, and all presentation issues are handled automatically for us, hence we do not need to concern ourselves with the view layout arrangements as long as we are happy with the order of columns in the view.

Reminder: What is MVC
model-view-controller (MVC) design is a design method where the information and the way it is rendered are separated, which is similar to the relationship between Gtk::TextView and Gtk::TextBuffer.

The obvious difference that needs little or no explanation is the fact that thetree storesare initialized with the "tree" rather than "list store" constructor (Gtk::TreeStore.new):

# Create a new tree model with three columns, as Boolean,
# integer and string.
store = Gtk::TreeStore.new(TrueClass, Integer, String)

However the most important difference is in how iterators, parent and child rows are handled. The first change that became an important feature in thestoredata initialization process was a new attribute called "@product_type" in the GroceryItem class. This attribute decides whether a data item (a row) is a product_category designated as the parent row in our data structure, or a child row for a particular product category.

Hence, when we are creating rows we have to create a row of parents and a row of children. The two are created in a slightly different way. The first difference is in the argument to the row creation method Gtk::TreeStore#append(parent). Namely, if the argument, here called parent, is nil we are creating parent row, when it is a non-nil Gtk::TreeIter we are creating a child of the parent to which the Gtk::TreeIter attribute points. Gtk::TreeStore#append method returns an iterator which points to the newly created empty data item (row). This iterator is then used to assign values to the columns in the empty row. We add three columns, hence three lines with either the method Gtk::TreeIter#[column]= or Gtk::TreeIter#set_value(iter, column, value). Let's look at the code fragment that illustrates the above narrative:

Initialize rows with data:

# Add all of the products to the GtkTreeStore.
list.each_with_index do |e, i|

  if (e.product_type == P_CATEGORY)
       . . .
    # Add the category as a new root (parent) row (element).
    parent = store.append(nil)

    parent[BUY_INDEX]  = list[i].buy       # same as:# store.set_value(parent, BUY_INDEX,  list[i].buy)
    parent[QTY_INDEX]  = list[i].quantity  # same as:# store.set_value(parent, QTY_INDEX,  list[i].quantity)
    parent[PROD_INDEX] = list[i].product   # same as:# store.set_value(parent, Prod_index, list[i].product)

  # Otherwise, add the product as a child row of the category.
  else
    child = store.append(parent)

    child[BUY_INDEX]  = list[i].buy       # same as:# store.set_value(child, BUY_INDEX,  list[i].buy)
    child[QTY_INDEX]  = list[i].quantity  # same as:# store.set_value(child, QTY_INDEX,  list[i].quantity)
    child[PROD_INDEX] = list[i].product   # same as:# store.set_value(child, Prod_index, list[i].product)
  end
end

Data Organization Within a Tree Store

One important thing to notice is the sequential data organization within the tree store, and bundling of products that belong to the same category in groups immediately after their respective parents. This reflects the way our arraylistis set-up.

list = Array.new
#                         Category    Buy    Qty  Product
list[0] = GroceryItem.new(P_CATEGORY, true,  0,   "Cleaning Supplies")
list[1] = GroceryItem.new(P_CHILD,    true,  1,   "Paper Towels")
list[2] = GroceryItem.new(P_CHILD,    true,  3,   "Toilet Paper")
list[3] = GroceryItem.new(P_CATEGORY, true,  0,   "Food")
list[4] = GroceryItem.new(P_CHILD,    true,  2,   "Bread")
list[5] = GroceryItem.new(P_CHILD,    false, 1,   "Butter")
list[6] = GroceryItem.new(P_CHILD,    true,  1,   "Milk")
list[7] = GroceryItem.new(P_CHILD,    false, 3,   "Chips")
list[8] = GroceryItem.new(P_CHILD,    true,  4,   "Soda")

In our example program we exploit the above sequential organization and the bundling together of all the children of different categories in the loop calculating the totals for the parents by iterating through their groups of children and changing the "quantity" property of the GroceryItem object when it is of type product category:

list.each_with_index do |e, i|
  # If the product type is a category, count the quantity
  # of all of the products in the category that are going
  # to be bought.
  if (e.product_type == P_CATEGORY)
    j = i + 1
    # Calculate how many products will be bought in
    # the category.
    while j < list.size && list[j].product_type != P_CATEGORY
      list[i].quantity += list[j].quantity if list[j].buy
      j += 1
    end
  . . .
end

The same facts about the sequential order and grouping of product categories of our data is exploited also when we load the data form GroceryItem objects in the initialization array (list) into the tree view and create its parent and child rows:

# Add all of the products to the GtkTreeStore.
list.each_with_index do |e, i|
  if (e.product_type == P_CATEGORY)
    # Add the category as a new root (parent) row (element).
    parent = store.append(nil)
    parent[BUY_INDEX]  = list[i].buy
    parent[QTY_INDEX]  = list[i].quantity
    parent[PROD_INDEX] = list[i].product
  # Otherwise, add the product as a child row of the category.
  else
    child = store.append(parent)
    child[BUY_INDEX]  = list[i].buy
    child[QTY_INDEX]  = list[i].quantity
    child[PROD_INDEX] = list[i].product
  end
end

Building a Multilayered Tree Store

By now you must have noticed the difference in code when building a list store and when building a tree store. Namely, the Gtk::ListStore#append and Gtk::TreeStore#append(node):

Adding Rows to List Store

# List Store
iter = store.append

Adding Rows to Tree Store

# Tree Store
iter = store.append(node)   # where node=nil or node=parent and iter becomes either parent or child respectively

In either case above, theiterpoints to a newly added empty row in list or tree store. In both cases you need the returnediterto initialize the newly added empty row with data (see the code segment entitled"Initialize rows with data:"above.

Here, in this paper, we have only looked at the issue of building or rather initializing a tree view. However, this is only the tip of an iceberg, namely, things get much more involved when we wish to add or remove rows at random, and there is an entire new article devoted to adding and removing rows.

Section devoted to adding and removing rows:

Following is promised section about the Cell Data Function.

Cell Data Function

Cell Data Function is such a prominent tree view feature that it deserves to be treated on its own. But it is also so tightly related to the tree view cells and renderers, that we will cover it in its entirety within 'Using Gtk::TreeStore' segment. We also have already in the preceding segment covered other two 'cell data function' closely related tree view elements, namely, the 'Gtk Tree View Columns and Cell Renderers', so we should have no trouble sifting through all these topics when they overlap. With the exception of 'Cell Data Function' API documentation, you will seldom if ever talk about 'Cell Data Function' in isolation, since it is only utilized to effect or alter the tree view cells or cellrenderers, hence, we might as well treat this feature as part of the tree view and the rest of column processing gadgets.

Therefor, not surprisingly, if you need to further customize a cell before it is rendered to the screen, you can use cell data function Gtk::TreeViewColumn#set_cell_data_func(cell) {|tvc, cell, model, iter| ... }. This facility provides means to modify cell-renderer's attributes for each individual tree view cell. For example, you can set a background colour based on the content of the cell, or format the numeric value perhaps only to restrict a number of decimal places for a floating point. It can also be used to set cellrenderer's properties that are calculated during the run time.

Caution:
Make sure you do not use Gtk::TreeViewColumn#set_cell_data_func if you have a large number of rows in your tree model. Cell data functions are invoked on every cell in the column before it is rendered, so they can significantly slow down tree models.

The main part of this mechanism known as 'Cell Data Function' is a block of code that is called for any desired cell renderer in a selected column for each single row before that row is rendered. Since the setting for a cell-renderer's property remains in effect for the entire column from the point (i.e. row) where it was set to the point (row) where it is overridden by a new setting, you need to carefully plan when to set and when to reset them. For instance if you wanted to render a background of each row in a tree view in different colour, you'd have to set the cell renderer's background property for every cell renderer in that row to a desired colour. Which means that there has to be a place that each cell-renderer will consult each time just before it is about to render its data. This place is the code block defined for a Gtk::TreeViewColumn#set_cell_data_func. Every cell on each row of a column in the tree view can, if you set it for the column, trigger a call to this method with its associated code block. In our instance, just mentioned above, where we would like to render the background of all the rows in a different colour, every cell in the row for as many rows in the tree view as the altering of the colour is required, would need to invoke "set_cell_data_func" with an appropriate code block. Indeed, many times you would want to render only some particular cell, or perhaps a few cells, with certain cell-renderer's properties set to different values than are set for the rest of the cells in the tree view. Shortly, we will see examples for both of these situations. But let's look at how these features, that allow us to so flexibly modify cell renderer attributes, fit together.

Situations when certain cells in a tree view need to be presented differently than the rest of them inevitably reveal a tighter than usual relationship between the model and the view. We have seen the evidence of this tighter relationship between the model and the view on previous page in the paragraph entitled "Dissecting the column creation" when discussing the hash parameter for cell-renderer's attributes where attributes were passed to Gtk::TreeViewColumn object's constructor and to the Gtk::TreeViewColumn#set_attributes instance method, as a set of name/value pairs, for whose values we were adamantly insisting were not the values of these very attributes but rather for the column numbers of their respective values stored in themodel.

Reminder:

Not, that earlier mentioned "tighter relationship between the tree view and the model" is directly related to "set_cell_data_func", it (namely, the tighter relationship) is only very often exploited inside the associated code block of cell data function. So let us recall how these hash arguments looked like, and in what context were they used:

column   = Gtk::TreeViewColumn.new("Color Name", renderer, { :text => COLOR_NAME, :foreground => NAME_FG_COLOR })
  # or elsewhere:
  # column.set_attributes(renderer, {:text => PRODUCT, :background => 5})

And how the above "tree view" code is related to the model:

list.each_with_index do |e, i|
  iter = store.append
         # . . .
  iter[NAME_FG_COLOR] = list[i].fg
end

When we first started to discuss the renderers and their attributes we were not yet able to venture into any meaningful code examples precisely because all these examples utilized 'set_cell_data_func'. It was also hard to deal with the model/view relationships because we lacked basic understanding of the two. We now know how columns in a model are related to the columns in a view, and particularly we know that there is not a one-to-one mapping between them, hence, it is now easier to grasp the meaning of the above code snippets. If you still have troubles understanding these issues, perhaps you should reread the introductory chapter with the "Dissecting the column creation" paragraph in it. In the first of the above two code snippets we can see how tree view 'column objects' are associated with 'cell renderer objects', and that the attributes are actually referred to their pertinent cells in model columns, where their values, potentially many for a single column (one-to-many relationship), should reside in their respective columns across different model rows. Therefor, not only can there be more than a single attribute associated with a single cell renderer, a single attribute can have different values assigned to it in different rows within a particular column in the model. This organization calls for a mechanism like our 'set_cell_data_func' and its code block which are executed for each row in the tree view, so those different values each attribute can have stored in different rows may be read and loaded.

Most of what was just said is encapsulated it the second of the two program examples below called "modelNview_demo.rb". But let's not get ahead of ourselves here and concentrate on the simpler version of the two, where we do not store the values for renders' attributes in the model, but change the desired property according to the different data values it renders. Indeed, the data being displayed and consulted resides in the persistent model. The first of the two programs is our "Grocery List" example we have already seen, this time with one new feature, namely, we want to highlight only those rows for which the "Buy" column cells contain TRUE, by turning the background of these cells to red.

Not surprisingly all the work to set the renderer's background property is done in the code block associated with the set_cell_data_func method. Note that this method is passed a cell renderer, which associates it with this particular renderer, which in turn is mapped to a tree view column via the column constructor (Gtk::TreeViewColumn.new), and subsequently by the Gtk::TreeView#append_column(column) statement to the tree view; so all render, column, and thetree vieware all connected (i.e. related.) The block associated with the "cell data function" is executed for every row for that particular view column. However, what may surprise a novice GTK+ programmer is that there is no:backgroundattribute present in the hash of attribute name/value pairs in the constructor for the column!

column   = Gtk::TreeViewColumn.new("Buy", renderer,  :text => BUY_IT)

Do not be alarmed if you too are disturbed by the fact that there is no ':background' attribute present here. There is a good chance next few paragraphs and the examples will clear things up.



liststore-cdFunc.png

In the first (Grocery List) program, in the code block associated withset_cell_data_funcmethod we are checking if 'Buy' column contains TRUE. When it does, the background of the column is set to red, otherwise the nil value forces the background to revert to default. Note, that every cell renderer is always aware of two places where the data resides, namely in the model and in the view. In the case of our 'Grocery List' program, (1) the first place is the persistent data in the model column, of which the renderer is made aware of by the column constructor, when in the form of hash argument the model column number for the:textattribute is passed to it, and (2) the second place is the column in the tree view, for which the cell renderer itself is responsible to copy over the value from the model for every row it renders.

Obviously, in the tree model there are as many different 'Buy' column values as there are rows, and each value in the model has a corresponding value on the display or in the tree view. Depending on the value in each row, the renderer has to render the background either white or red. This happens in theset_cell_data_funcmethod which is run for every row in the model. When you become familiar with tree view, you should be able to obtain the same information by reading the following code snippet:

renderer = Gtk::CellRendererText.new
column   = Gtk::TreeViewColumn.new("Buy", renderer,  :text => BUY_IT)
column.set_cell_data_func(renderer) do |col, renderer, model, iter|
  renderer.background = iter[BUY_IT] ? "red" : nil
end

As opposed to this program, where a single column of cells is only affected by different rendering style, in the second 'Model/View Column Demo' program an effort is made to control the entire row as well as a particular cell in the 'Color Name' column. Both programs nicely demonstrate how the helper cell data function and the associated code block are executed for each renderer, affecting each cell of the designated columns in every row for the tree store and subsequently also the view.

First, here is the listing of the entire 'Grocery List' program we discussed above:

liststore-cdFunc.rb (Grocery List)

#!/usr/bin/env ruby
require 'gtk2'

# Add three columns to the GtkTreeView. All three of the
# columns will be displayed as text, although one is a boolean
# value and another is an integer.
def setup_tree_view(treeview)
  # Create a new GtkCellRendererText, add it to the tree
  # view column and append the column to the tree view.
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Buy", renderer,  :text => BUY_IT)
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.background = iter[BUY_IT] ? "red" : nil
  end
  treeview.append_column(column)

  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Count", renderer, :text => QUANTITY)
  treeview.append_column(column)

  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Product", renderer, :text => PRODUCT)
  treeview.append_column(column)
end

class GroceryItem
  attr_accessor :buy, :quantity, :product
  def initialize(b, q, p); @buy, @quantity, @product = b, q, p; end
end
BUY_IT = 0; QUANTITY = 1; PRODUCT  = 2

list = Array.new
list[0] = GroceryItem.new(true,  1, "Paper Towels") 
list[1] = GroceryItem.new(true,  2, "Bread")
list[2] = GroceryItem.new(false, 1, "Butter")
list[3] = GroceryItem.new(true,  1, "Milk")
list[4] = GroceryItem.new(false, 3, "Chips")
list[5] = GroceryItem.new(true,  4, "Soda") 

# Create a new tree model with three columns, as Boolean,
# integer and string.
store = Gtk::ListStore.new(TrueClass, Integer, String)
treeview = Gtk::TreeView.new(store) # Add the tree model (store) to the tree view
setup_tree_view(treeview)

# Add all of the products to the GtkListStore.
list.each_with_index do |e, i|
    iter = store.append
    store.set_value(iter, BUY_IT,   list[i].buy)	# iter[BUY_IT]   = list[i].buy
    store.set_value(iter, QUANTITY, list[i].quantity)	# iter[QUANTITY] = list[i].quantity
    store.set_value(iter, PRODUCT,  list[i].product)	# iter[PRODUCT]  = list[i].product
end

scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)

window = Gtk::Window.new("Grocery List")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(250, 165)
window.add(scrolled_win)
window.show_all
Gtk.main



modelNview_demo.png

Finally, we have reached the 'Model/View Column Demo' example program which does not only handle individual cells or the complete columns, but affects all of them across the entire row for all rows in the model and view. We already encountered the introduction to this program on the previous page where we first touched upon the fact that a tree model column should not be confused with a tree view column.



Note that in this example in the model there are four columns whereas in tree view there are just two.

treeview = Gtk::TreeView.new(store = Gtk::ListStore.new(Integer, String, String, String))

The renderer attributes here are used to store system parameters, namely, the foreground and background colours for the two tree view columns. Not only that, different rows in the model store different colours values for these two attributes. As previously announced, in this example program entire row is manipulated with their respective set_cell_data_funcs blocks in which the background and foreground attributes are set differently for each row. That is, all columns for all the cell renderers in this tree view trigger execution of their respective 'set_cell_data_func' methods and code blocks. What is also interesting is how in some of these blocks multiple data (i.e. multiple renderer's parameters) in the same row from different model columns are consulted. If you understand this program, you should also understand that model columns do not map to view columns.

modelNview_demo.rb ("Model/View Column Demo")

#!/usr/bin/env ruby
require 'gtk2'

def setup_tree_view(treeview)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Row Number", renderer, { :text => ROW_NO, :background => ROW_BG_COLOR })
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.background = iter[ROW_BG_COLOR]
  end
  treeview.append_column(column)

  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Color Name", renderer, { :text => COLOR_NAME, :foreground => NAME_FG_COLOR })
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.foreground  = iter[NAME_FG_COLOR]
    renderer.background = iter[ROW_BG_COLOR]
  end
  treeview.append_column(column)
end

window = Gtk::Window.new("Model/View Column Demo")
window.resizable = true
window.border_width = 10

window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(300, -1)

class ColorListItem
  attr_accessor :rno, :bg, :name, :fg
  def initialize(rno, bg, name, fg); @rno, @bg, @name, @fg = rno, bg, name, fg; end
end
ROW_NO = 0; ROW_BG_COLOR = 1; COLOR_NAME = 2; NAME_FG_COLOR = 3

list = Array.new
list[0] = ColorListItem.new(1, "lightblue",   "Red",    "#ff0000") 
list[1] = ColorListItem.new(2, "lightgreen",  "Blue",   "#0000ff")
list[2] = ColorListItem.new(3, "lightblue",   "Green",  "#00ff00")
list[3] = ColorListItem.new(4, "lightgreen",  "Black",  "#000000")
list[4] = ColorListItem.new(5, "lightblue",   "Purple", "#9900ff")

treeview = Gtk::TreeView.new(store = Gtk::ListStore.new(Integer, String, String, String))
setup_tree_view(treeview)

list.each_with_index do |e, i|
  iter = store.append
  iter[ROW_NO]        = list[i].rno  # same as: >>> # store.set_value(iter, ROW_NO, list[i].rno)
  iter[ROW_BG_COLOR]  = list[i].bg
  iter[COLOR_NAME]    = list[i].name
  iter[NAME_FG_COLOR] = list[i].fg
end

window.add(treeview)
window.show_all
Gtk.main

Multidimensional Tree Store

treestore-multi-pathNdepth.png

Now that we have learned the basics of list and tree store mechanics we can look at a very simple example in which all rows are built by the most basic tools without any automation. We also employ the cell data function to display the cell position within the hierarchy utilizing thepathanddepthinstance methods defined in Gtk::TreeIter and Gtk::TreePath classes respectively.


manually-build-multilevel-tview-w-depthNpath.rb

#!/usr/bin/env ruby
require 'gtk2'

treestore = Gtk::TreeStore.new(String, String)

# Append a toplevel row and fill in some data
parent = treestore.append(nil)
parent[0] = "Maria"
parent[1] = "Incognito"
# Append another toplevel row and fill in some data
parent = treestore.append(nil)
parent[0] = "Tony"
parent[1] = "Zus"
# Append a second toplevel row and fill in some data
parent = treestore.append(nil)
parent[0] = "Jane"
parent[1] = "Average"
# Append a child to the second toplevel row and fill in some data
child = treestore.append(parent)
child[0] = "Janinita"
child[1] = "Average"
# Append a grandchild to the third level row and fill in some data
grandchild = treestore.append(child)
grandchild[0] = "Janinitica"
grandchild[1] = "Average"
# Append the 2nd grandchild to the third level row and fill in some data
grandchild = treestore.append(child)
grandchild[0] = "Janinitica2"
grandchild[1] = "Average"
# Append again a second toplevel row and fill in some data
parent = treestore.append(nil)
parent[0] = "Gustav"
parent[1] = "Crocky"
# Append a child to the second toplevel row and fill in some data
child = treestore.append(parent)
child[0] = "Gustlek"
child[1] = "Crocky"
# Append a child to the second toplevel row and fill in some data
child = treestore.append(parent)
child[0] = "Zyla"
child[1] = "Crocky"

view = Gtk::TreeView.new(treestore)
view.selection.mode = Gtk::SELECTION_NONE

# Create a renderer
renderer = Gtk::CellRendererText.new
# Add column using our renderer
col = Gtk::TreeViewColumn.new("First Name", renderer, :text => 0)
view.append_column(col)

# Create another renderer and set the weight property
renderer = Gtk::CellRendererText.new
# Add column using the second renderer
col = Gtk::TreeViewColumn.new("Last Name", renderer, :text => 1)
view.append_column(col)

# - add another (virtual) VIEW column to TreeView
# Create one last renderer and set its display value to row's path
renderer = Gtk::CellRendererText.new
# Add column using the third renderer
col = Gtk::TreeViewColumn.new("Depth & Path", renderer)
view.append_column(col)

# Create a cell data function to display row path and depth.
col.set_cell_data_func(renderer) do |col, renderer, model, iter|
  renderer.text = "D=#{iter.path.depth}\tP=#{iter.path.to_str}"
end

window = Gtk::Window.new("Multilevel Tree View")
window.signal_connect("destroy") { Gtk.main_quit }
window.add(view)
window.show_all
Gtk.main

Tedious Job Of Loading Multidimensional Tree Store

Loading initial values into a tree store may become a daunting task for large tree hierarchies. We usually can help ourselves with some kind of organized structure of ASCII data, most likely a nicely laid out array. Following is such an example. For instance we can design a table where the hierarchy is defined as a structure of nested hierarchies of arrays. The signature of this design is the last element in each row, namely we define it either to be the array of children or nil:

init_arr = [[ e11, e12, e13, nil ], [e21, e22, e23, [[...],[...]...], ... [en1, en2, en3, nil]] 

Following is a more realistic example:

# DATA  INITIALIZATION  ARRAY:
# Product,   Price,  Children
# ---------------------------------------------
INITIALIZATION_ARRAY = [
   ['Computer A101dt',  0,  [
       ['Keyboard',       50.0,  nil],
       ['Mouse',          40.0,  nil],
       ['Computer Case',  0,    [
            ['Front 120mm Fan', 10.0,   nil],
            ['Back  120mm Fan', 10.0,   nil],
            ['PW/S',            270.0,  nil],
            ] ],
       ['Mother Board',        250.0,    nil],
       ['CPU',                 300.0,    nil],
       ['1 TB HD',             170.0,    nil],
       ] ],
   ['Swan 27" Monitor ',    210.0,  nil],
   ['Laser Printer XL3500', 300.0,  nil],
 ]

In above initialization structure we have an array of three element arrays with columns "Product", "Price", and "Children", where the last element (Children) of each row, can either have an embedded array of arrays of the same kind we are describing here, or it is nil.

Looking at this array, we can not avoid to overlook the properties and likely also the behaviour of a composite design pattern, where each elementary array (row) looks and when processed most likely also behaves exactly like all its parents and children providing it has them.

It would be possible to load this kind of array directly into a Gtk::TreeStore, however, if we plan to also perform some kind of data processing at the same time, for instance accumulating the price for each of the parent rows from all their respective children, we would be much better off converting the initialization array into an object, especially with the promise to employ well known object-oriented programming tools such as Composite Design Pattern.

Converting an appropriately designed array into a composite object in Ruby is simple. Following is the blueprint for such a composite object; lets call it Products:

class Products
  attr_accessor :product, :price, :children
  def initialize(product, price, children)
    @product, @price, @children = product, price, children
    @price = 0 if @price == nil
    arr = []
    if @children
      @children.each do |row|
        arr << Products.new(*row)
      end
      @children = arr
    end
  end
end


composite-ptt-s75-2x.jpg

In the absence of the Leaf and the Composite objects from the well known GoF blueprint design diagram, seen on the image on the right here, it is hard to conclude ourProductsobject truly represents theComposite Design Pattern.However, if you look closely, you will find all the required objects and programming design elements needed in this pattern in our "Products" object. When our Children element contains thenilwe have aLeafobject, else it is theComposite,and all together is theComponent with the abstract operationprice.Those of you new to Ruby should take into account, that abstract classes and operations in Ruby seldom if ever are explicitly defined. This is all part of the strategy calledduck typing,namely, the language does no checking to ensure that an object being passed around has any particular class ancestry. This is known as Ruby dynamic typing (hence, if it quacks like a duck, it must be a duck).

You know, we would like to calculate the complete price for each composite product on all levels; i.e. just as we would like to know what is the price of the entire computer, we also wish to know what is the price for its composite parts such as the Computer Case. This total gathering method has the potential to become the missingoperationmethod, seen on the diagram above, that will provide the solid evidence we really are dealing with theComposite Design Pattern.In order to write the customized price getter method, which will behave as the proposed GoF'soperation()method, we need to modify the way we currently define theattr_accessormethods:

attr_accessor :product, :children
attr_writer :price

Finally, we can tuck away the entire ascii initialization mechanism and the details of our Composite Design PatternProducts object into a module. We name it "initialize-products-from-asci-table.rb":

MODULE: initialize-products-from-asci-table.rb

module InitializeProductsFromASCItable

  # DATA  INITIALIZATION  ARRAY:
  # Product,   Price,  Children
  # ---------------------------------------------
  INIT_ARRAY = [
    ['Computer A101dt',  nil,  [
        ['Keyboard',       50.0,  nil],
        ['Mouse',          40.0,  nil],
        ['Computer Case',  nil,    [
             ['Chacy',           nil,  [
                   ['Inner Skeleton', 99.99,    nil],
                   ['Sound Insolation', nil,  [
                         ['Side panels',     50.99,  nil],
                         ['Chock Absorbers', 50.99,  nil],
                         ] ],
                   ] ],
             ['Front 120mm Fan', 10.0,   nil],
             ['Door   80mm Fan', 10.0,   nil],
             ['Back  120mm Fan', 10.0,   nil],
             ['PW/S',            270.0,  nil],
             ] ],
        ['Mother Board',        250.0,    nil],
        ['16 GB SD RAM',        200.0,    nil],
        ['CPU',                 300.0,    nil],
        ['CPU Cooler',          200.0,    nil],
        ['RW/DVD',              30.0,     nil],
        ['1 TB HD',             170.0,    nil],
        ['500 MB HD',           60.0,     nil],
        ['RADEON Graphic Card', 300.0,    nil],
        ] ],
    ['Swan 27" Monitor ',    210.0,  nil],
    ['Laser Printer XL3500', 300.0,  nil],
  ]

  class Products
    attr_accessor :product, :children
    attr_writer :price
    def initialize(product, price, children)
      @product, @price, @children = product, price, children
      @price = 0 if @price == nil
      arr = []
      if @children
        @children.each do |row|
          arr << Products.new(*row)
        end
        @children = arr
      end
    end

    def price
      price = 0.0
      if @children
        @children.each { |c| price += c.price }
      else
        price = @price
      end
      price
    end
  end
end


treev-MultiDim-load-asci-table.png Following is the program, called "treev-MultiDim-load-asci-table.rb", that displays the above array of arrays, we created in a separate module, the screenshot of which you cam see here on the left. Indeed, to run it you have to download both the above module ("initialize-products-from-asci-table.rb") and the following program into the same directory. This program works just as expected. However, because we dynamically process some of its data, namely by nature, the accumulative price, it also requires a special attention, when adding or removing rows. The addition and deletion of records in a tree store is a serious matter, even when we do not have dynamically changed data, and will be dealt with only after we have a second look at Gtk::TreePath, Gtk::TreeIter, and Gtk::TreeRowReference. You can guess, on the following pages we will encounter a second version of this program, demonstrating how to handle adding and deleting rows.


treev-MultiDim-load-asci-table.rb

#!/usr/bin/env ruby
require 'gtk2'

$: << "."
require "initialize-products-from-asci-table.rb"
include InitializeProductsFromASCItable

product_list = []
INIT_ARRAY.each_with_index do |row, i|
  product_list[i] = Products.new(*row)
end
product_list.each {|p| print "DEBUG: %-20s ..... %07.2f\n" % [p.product, p.price] }

def setup_tree_view(treeview)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Product Name", renderer, :text => NAME_COLUMN)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Price", renderer, :text => PRICE_COLUMN)
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.text  = "%07.2f" % iter[PRICE_COLUMN]
  end
  treeview.append_column(column)
end

NAME_COLUMN, PRICE_COLUMN = 0, 1
treeview = Gtk::TreeView.new(store = Gtk::TreeStore.new(String, Float))
setup_tree_view(treeview)

def load_products(store, product_list, parent)

  # Add all of the products to the GtkTreeStore.
  product_list.each do |prod_obj|

    # If the product has children it's a parent
    if (prod_obj.children)
      # Add the category as a new root (parent) row (element).
      child_parent = store.append(parent)
      # store.set_value(parent, COL, value) # <= same as below
      child_parent[NAME_COLUMN]   = prod_obj.product
      child_parent[PRICE_COLUMN]  = prod_obj.price
      load_products(store, prod_obj.children, child_parent)

    # Otherwise, add the product as a child row of the category.
    else
      child = store.append(parent)
      # store.set_value(child, COL, value) # <= same as below
      child[NAME_COLUMN]   = prod_obj.product
      child[PRICE_COLUMN]  = prod_obj.price
      next
    end
  end
end

load_products(store, product_list, nil)

window = Gtk::Window.new("Tree View Depth Demo")
window.resizable = true

window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(300, -1)
scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
window.add(scrolled_win)
window.show_all
Gtk.main



Multi-item Super Columns

When discussing Cell Renderers (on the previous page), we said: "Cell renderers are packed into tree view columns similar to how you add widgets into horizontal boxes", and also promised to explain this feature.

multi-item-column.png

go-home.png redo.png Not really required, but nevertheless desirable first thing you need to do if you wish to run this program and see the same output as on the figure on the right, is that you copy the two images here on the left that are used in the program into the directory where your source code file for this program will resides. You should name them go-home.png and redo.png. You can copy them from this page by right-clicking on each, and select "Copy image" from the context menu in your browser, or if you for some reason have trouble doing that make 2 small pictures of anything yourself. Just make sure the dimensions are not not more than 25x25 pixels. Note that in this example program there is a third picture (the red traffic sign) used, however, you do not need to make a copy of this one, because it is used as GTK+ stock item (see: Gtk::Stock::DIALOG_ERROR in the listing).


liststore-col-w-double-renderers-pixbufs.rb

#!/usr/bin/env ruby
require 'gtk2'

def setup_tree_view(treeview)
  renderer = Gtk::CellRendererText.new
  renderer.foreground = "#ff0000"
  column   = Gtk::TreeViewColumn.new("Buy", renderer,  :text => BUY_IT)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Count", renderer, :text => QUANTITY)
  treeview.append_column(column)

# column   = Gtk::TreeViewColumn.new("Product", renderer, 'text' => PRODUCT)
  column   = Gtk::TreeViewColumn.new
  column.title = "Product /w icons"
  pixb_renderer = Gtk::CellRendererPixbuf.new
  renderer = Gtk::CellRendererText.new
  column.pack_start(pixb_renderer, false)
  column.pack_start(renderer, false)
  column.set_cell_data_func(pixb_renderer) do |col, renderer, model, iter|
    renderer.stock_id = Gtk::Stock::DIALOG_ERROR if iter[ICON] == nil
  end

  # PECULIARITY: You can only add/set attributes after they were packed
  # -- You can use either add_... or set_... attribute/s method; the difference
  # -- between these two methods (add_/set_) is in the number of attributes you 
  # -- need to set, a single one with the set_ or many with add_ method. 
 #column.add_attribute(pixb_renderer, :pixbuf, ICON)
  column.set_attributes(pixb_renderer, :pixbuf => ICON)
  column.add_attribute(renderer, :text, PRODUCT)
 #column.set_attributes(renderer, :text => PRODUCT)
  treeview.append_column(column)
end
def get_pixbuf_if_file_exists(file)
  begin
    pixbuf = Gdk::Pixbuf.new(file)
    rescue GLib::FileError => err
      print "I/O ERROR (%s): %s\n" % [err.class, err]
      pixbuf = nil
  end
  pixbuf
end
window = Gtk::Window.new("Test Multi-Item Column ")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(270, 185)

class GroceryItem
  attr_accessor :buy, :quantity, :product, :icon
  def initialize(b, q, p, i); @buy, @quantity, @product, @icon = b, q, p, i; end
end
BUY_IT = 0; QUANTITY = 1; PRODUCT  = 2; ICON=3
list = Array.new
list[0] = GroceryItem.new(true,  1, "Paper Towels", "redo.png") 
list[1] = GroceryItem.new(true,  1, "Dog House",    "go-home.png")
list[2] = GroceryItem.new(false, 1, "Butter",       nil)
list[3] = GroceryItem.new(true,  1, "Milk",         "redo.png")
list[4] = GroceryItem.new(false, 3, "Chips",        nil)
list[5] = GroceryItem.new(true,  4, "Soda",         nil) 
store = Gtk::ListStore.new(TrueClass, Integer, String, Gdk::Pixbuf)
treeview = Gtk::TreeView.new(store)
setup_tree_view(treeview)

list.each_with_index do |e, i|
  iter = store.append
  iter[BUY_IT]   = list[i].buy
  iter[QUANTITY] = list[i].quantity
  iter[PRODUCT]  = list[i].product
  iter[ICON] = get_pixbuf_if_file_exists(list[i].icon) if list[i].icon
end
scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
window.add(scrolled_win)
window.show_all
Gtk.main

There are a few places in the "liststore.rb" listing, that changed to produce new "Multi-item Column" version. We can start with the expansion of the model. Namely, we had to add the new Gdk::Pixbuf model column:

store = Gtk::ListStore.new(TrueClass, Integer, String, Gdk::Pixbuf)

After the model structure was augmented we also need to provide and load the data into the newly created column. Accordingly the new column constant ICON is added and the column of image file names is added to the list array. These changes are also reflected in the data loading loop, where our model in store variable is initialized with a Gdk::Pixbuf object created from the image file. For this to work we need a helper method called get_pixbuf_if_file_exists, which has to make sure in event image files do not exist errors are handled gracefully.

But the most important changes for this "Multi-item Column" version program are tucked away in the setup_tree_view method, where we needed to replace the original column instantiation method which handled a single "Product" item with a number of different methods that now manage both text and image items in the same column. This also requires reordering of statements dictated first by the API, and second by the way we would like to arrange the order of items (ie text and images) in the Product column. For the API related reordering of code please check the listing and search for the comment (# PECULIARITY: ...). The ordering of image and text in the column deserves some more attention. If you didn't care weather image appeared before the string (text), you wouldn't have to replace the original Product column instantiation statement:

column   = Gtk::TreeViewColumn.new("Product", renderer, :text => PRODUCT)

You could simply pack the "pixb_renderer" into it, after initialization in which the specified renderer is also set to take the first position in the left side of the in the cell (note that packing order can not alter this positional arrangement, the text would remain to sit before the image in tree view column). We chose the other option, namely, to position the image before the text. This is why we have to manually build both renderers (one for the image and the other for text) and then pack them into a column in order we like!

Alternatively, you could also start up with the pixbuf renderer, and instantiate its column, to which you would then have to add (pack) the text renderer:

pixb_renderer = Gtk::CellRendererPixbuf.new
renderer = Gtk::CellRendererText.new
column = Gtk::TreeViewColumn.new("Product /w icons", pixb_renderer, :pixbuf => ICON)
column.pack_start(renderer, false)
column.set_cell_data_func(pixb_renderer) do |col, renderer, model, iter|
  renderer.stock_id = Gtk::Stock::DIALOG_ERROR if iter[ICON] == nil
end
column.add_attribute(renderer, :text, PRODUCT)
treeview.append_column(column)

Yet another wrinkle in our column building process is handling the situations when image is not available. We chose to complain with an annoying ERROR image. This is done with the stock item as follows:

column.set_cell_data_func(pixb_renderer) do |col, renderer, model, iter|
  renderer.stock_id = Gtk::Stock::DIALOG_ERROR if iter[ICON] == nil
end

Indeed, to understand this latest item you need to know about Cell Data Functions explained above.