Create  Edit  FrontPage  Index  Search  Changes  History  RSS  Login

tut-gtk2-treev-crs

Cell Renderers:

With the exception of the Gtk::CellRenderer class in the section [8.1.2] entitled 'Gtk TreeViewColumn and Gtk CellRenderer', where we extensively talked about renderers in general, and the Gtk::CellRendererPixbuf class in the section [8.2.4] under the title 'Multi-item Super Columns' you have, up to this point, learned only about one type of cell renderer, namely, the Gtk::CellRendererText. This renderer allows you to display strings, numbers and Boolean values as text. You are able to customize how the text is displayed with cell renderer attributes and cell data functions and allow it to be edited by the user.

GTK+ provides a large number of cell renderers that can display other types of widgets beside text. Following is the list are of all of the cell renderers, about two the Gtk::CellRenderer and Gtk::CellRendererText you have already learned, but the rest of which will all be covered in the reminder of this chapter.

Cell Renderers:
Still to be covered:

Toggle Button Renderers

Displaying Boolean values as "TRUE" or "FALSE" with Gtk::CellRendererText, especially in large tables with many Boolean columns, becomes soon very illegible and aesthetic, as well as it also takes up a too much valuable visible area. You would significantly improve lucidity of your Boolean tables if instead of text strings you displayed a check button for Boolean values. As it turns out you can do just that, with the help of the toggle button cell renderers called Gtk::CellRendererToggle. By default, they are drawn as check buttons, but can also be set up as radio buttons, however, that would also require you yourself implement the necessary radio button functionality.

As with the editable text renderers, you have to manually apply the changes performed by the user. Otherwise, the button will not toggle visually on the screen. Because of this, Gtk::CellRendererToggle provides thetoggledsignal, which is emitted when the user presses the check button.

renderer.signal_connect('toggled') do |w, path|
  iter = treeview.model.get_iter(path)
  iter[ITEM_COLUMN] = !iter[ITEM_COLUMN] if (iter)
end

With the introduction of toggle button cell renderers you actually make the Boolean column editable, which means you can interactively modify its contents in the model, and subsequently its presentation in the tree view. If the Boolean value is in any way related to the contents of other columns in the tree view this dependency may have to be managed too. As you will discover shortly, our example program has such a dependency. Not to bury the mere mechanics of implementing toggle button cell renderers in side issues, we will approach the toggle button renderer itself and its data dependency related issues in two steps. First we will introduce the the toggle renderer without thinking how it may effect the presentation of other data in the tree view (toggle-rndr-1.rb), and in the second example (toggle-rndr-2.rb), immediately after that, we will tackle those dependency related issues.

treev-crs-01.png

As you can see from the image on the right we are continuing to use the same program example as in the introductory session to the tree store (1.3 Using Gtk::ListStore). The example program there we called 'liststore.rb', which then in the following section (Using Gtk::TreeStore) got the present "tree-view shape" and was more appropriately called 'treestore.rb'. The only changes in the program here are in the setup_tree_view(treeview) method, where we had to change the type of our cell renderer. An important, and easily overlooked change is also in the column line, where we change the name of the attribute that used to be "text" to "active". Since we wish to provide our users with the ability to interactively change the status of "Buy" column, we need to make our toggle button clickable, which means that we also need to provide the callback proc (block), triggered by the'toggled'signal, which will set the toggle to the new value.

The 'toggle-rndr-1.rb' listing presents our 'Grocery List' application with the just mentioned callback code block. Note that for this renderer's'toggled'signal thepathfor the row on which the check-box is clicked is passed into the block as block parameter. In it, when 'toggled' signal is emitted, we alter the value in the model for the current row in the GItm::BUY_INDEX column, which subsequently effects the respective cell in the tree view as the toggle renderer renders it on the display.


toggle-rndr-1.rb

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

# Toggle Button Cell Renderer

# Add three columns to the GtkTreeView. This time first
# Boolean 'Buy' column will appear as check-button, the
# other two columns 'Count' and 'Product' remain Integer
# and String respectively.
def setup_tree_view(treeview)
  renderer = Gtk::CellRendererToggle.new
  column = Gtk::TreeViewColumn.new("Buy", renderer, "active" => GItm::BUY_INDEX)
  renderer.signal_connect('toggled') do |w, path|
    iter = treeview.model.get_iter(path)
    iter[GItm::BUY_INDEX] = !iter[GItm::BUY_INDEX] if (iter)
  end
  treeview.append_column(column)

  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Count", renderer, "text" => GItm::QTY_INDEX)
  treeview.append_column(column) 
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Product", renderer, "text" => GItm::PROD_INDEX)
  treeview.append_column(column)
end

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

list = [
  GItm.new(GItm::PROD_CTG, true,  0, "Cleaning Supplies"),
  GItm.new(GItm::CHILD,    true,  1, "Paper Towels"),
  GItm.new(GItm::CHILD,    true,  3, "Toilet Paper"),
  GItm.new(GItm::PROD_CTG, true,  0, "Food"),
  GItm.new(GItm::CHILD,    true,  2, "Bread"),
  GItm.new(GItm::CHILD,    false, 1, "Butter"),
  GItm.new(GItm::CHILD,    true,  1, "Milk"),
  GItm.new(GItm::CHILD,    false, 3, "Chips"),
  GItm.new(GItm::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 GtkListStore.
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 == GItm::PROD_CTG)
    j = i + 1

    # Calculate how many products will be bought in
    # the category.
    while j < list.size && list[j].product_type != GItm::PROD_CTG
      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, GItm::BUY_INDEX, list[i].buy) # <= same as below
    parent[GItm::BUY_INDEX]  = list[i].buy
    parent[GItm::QTY_INDEX]  = list[i].quantity
    parent[GItm::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, GItm::BUY_INDEX, list[i].buy) # <= same as below
    child[GItm::BUY_INDEX]  = list[i].buy
    child[GItm::QTY_INDEX]  = list[i].quantity
    child[GItm::PROD_INDEX] = list[i].product
  end
end

# Add the tree model to the tree view
treeview.model = store
treeview.expand_all

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(275, 200)
window.add(scrolled_win)
window.show_all
Gtk.main

Toggle cell renderers are created with Gtk::CellRendererToggle.new. Next, note that this time theactiveproperty is added as a column attribute rather thantext,as used by the Gtk::CellRendererText:

column = Gtk::TreeViewColumn.new("Buy", renderer, "active" => GItm::BUY_INDEX)

You also must bind the Gtk::CellRendererToggle renderer object to'toggled'signal and connect it to the callback code block. The block receives the cell renderer and Gtk::TreePath string pointing to the row containing the toggle button.

If you wish to turn the check box toggle button into radio button you can accomplish this by setting the renderer's radio property to true:

renderer.radio = true

But if you do that, you will still have to implement the radio button functionality yourself, otherwise you are risking to confuse your users with inconsistent behaviour of your radio buttons.

Toggle Button Renderers May Effect Other Related Values In Tree View

Sometimes when certain cell value is changed by the user it may effect one or more other cells on the display indicating data related dependencies in the tree view. In our 'Grocery List' example such dependencies can be found. Remember that in any product category row the column 'Count' displays the number of individual items from that category for which their respective 'Buy' columns are checked, i.e. in their model columns they contain Boolean value TRUE.

toggle-rndr-2.png

For instance, lets consider the initial state of the 'Grocery List' display immediately after you start up the program, in which the row identified by product 'Paper Towels' originally had the 'Buy' column checked (TRUE), and in the 'Count' column value 1. At the same time the pertinent product category row (Cleaning Supplies), due to the 'Toilet Paper' row which is also checked and has value 3 in its 'Count' column, contains value 4 in its 'Count' column, and indeed the 'Buy' column checked. Should a user then toggle the 'Buy' column for 'Paper Towels' row to unchecked (FALSE), that would ultimately affect the pertinent product category's 'Count' value to display now decreased value 3. If however, the user also unchecks the 'Toilet Paper' row, we will witness two changes in the 'Cleaning Supplies' product category row, namely, its 'Count' value will become 0 (zero), and the 'Buy' column will become unchecked. This reveals two data dependencies in our tree view. But there is a third issue lurking behind these dependencies here, namely it should not be possible for a user to manually toggle the check button for any product category.

Let's see how all this is managed:


toggle-rndr-2.rb

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

# Show data dependent tree view behaviour (toggle button renderer).

# Add three columns to the GtkTreeView. This time first
# Boolean 'Buy' column will appear as check-button, the
# other two columns 'Count' and 'Product' remain Integer
# and String respectively.

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::CellRendererToggle.new
  column = Gtk::TreeViewColumn.new("Buy", renderer, "active" => GItm::BUY_INDEX)

  column.set_cell_data_func(renderer) do |tvc, cell, model, iter|
    iter[GItm::BUY_INDEX] = any_child_set_to_buy(iter) if iter.has_child?
  end

  renderer.signal_connect('toggled') do |w, path|
    iter = treeview.model.get_iter(path)
    iter[GItm::BUY_INDEX] = !iter[GItm::BUY_INDEX] if (iter)
  end
  treeview.append_column(column)

  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Count", renderer, "text" => GItm::QTY_INDEX)
  column.set_cell_data_func(renderer) do |tvc, cell, model, iter|
    fix_parents_total(iter) if !iter.has_child?
  end
  treeview.append_column(column)

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

def any_child_set_to_buy(parent)
  tmp_iter = parent.first_child
  return true if tmp_iter[GItm::BUY_INDEX]
  (return true if tmp_iter[GItm::BUY_INDEX]) while tmp_iter.next!
  return false
end

def fix_parents_total(iter)
  parent = iter.parent
  tmp_iter = parent.first_child
  total = tmp_iter[GItm::BUY_INDEX] ? tmp_iter[GItm::QTY_INDEX] : 0
  (total += tmp_iter[GItm::QTY_INDEX] if tmp_iter[GItm::BUY_INDEX]) while tmp_iter.next!
  parent[GItm::QTY_INDEX] = total
end

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

list = [
  GItm.new(GItm::PROD_CTG, true,  0, "Cleaning Supplies"),
  GItm.new(GItm::CHILD,    true,  1, "Paper Towels"),
  GItm.new(GItm::CHILD,    true,  3, "Toilet Paper"),
  GItm.new(GItm::PROD_CTG, true,  0, "Food"),
  GItm.new(GItm::CHILD,    true,  2, "Bread"),
  GItm.new(GItm::CHILD,    false, 1, "Butter"),
  GItm.new(GItm::CHILD,    true,  1, "Milk"),
  GItm.new(GItm::CHILD,    false, 3, "Chips"),
  GItm.new(GItm::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 GtkListStore.
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 == GItm::PROD_CTG)
    j = i + 1

    # Calculate how many products will be bought in
    # the category.
    while j < list.size && list[j].product_type != GItm::PROD_CTG
      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, GItm::BUY_INDEX, list[i].buy) # <= same as below
    parent[GItm::BUY_INDEX]  = list[i].buy
    parent[GItm::QTY_INDEX]  = list[i].quantity
    parent[GItm::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, GItm::BUY_INDEX, list[i].buy) # <= same as below
    child[GItm::BUY_INDEX]  = list[i].buy
    child[GItm::QTY_INDEX]  = list[i].quantity
    child[GItm::PROD_INDEX] = list[i].product
  end
end

# Add the tree model to the tree view
treeview.model = store
treeview.expand_all

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-2 (w/toggle btt.)")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(300, 200)
window.add(scrolled_win)
window.show_all
Gtk.main


Data Dependencies Between Cells In Different Columns And Rows In Tree View

All the differences between the two programs above are found in two places in the'setup_tree_view'method in which the three tree view columns 'Buy', 'Count' and 'Product' are set. However, though only two modifications were made in the second example program, a user can observe three behavioural differences: (1) neither of the two product category's 'Buy' columns respond to clicks on their respective check boxes, (2) toggling clickable products now adjusts the 'Count' value of their product category header record to reflect the number of selected items from the category, and (3) if all the products of a category are deselected, the check-box in their control record is also unchecked and as soon a s any item is re-checked the product category control record's check-box is also set as checked.

Tree View Iter Arithmetic
When user actions affect cells in different tree view rows, obviously references to their respective positions within the model need to be maintained and/or memorized. However, when all the children rows of a parent node have to be traversed you can expect some kind of 'tree view iter arithmetic' will have to be employed. This iter and path manipulation (arithmetic) is not new to us. We have already encountered it in quite a few places in section [8.4] (Adding Rows and Handling Selections), particularly in the 'treev-MultiDim-loadASCItable-add-n-rm-rows.rb' example program there, and in the subsection [8.4.5] (Check if children are removed along with the parent).

But let's return back to our 'toggle-rndr-2.rb' example program here. The first modification with regards to the original 'toggle-rndr-1.rb' example above, is found in setting up the 'Buy' column and the second one is in how the 'Count' column is set up. Let's start with the second one first. That is, where the code to set the 'Count' column is found. There, in 'toggle-rndr-2.rb', we find the following new piece of code:

column.set_cell_data_func(renderer) do |tvc, cell, model, iter|
  fix_parents_total(iter) if !iter.has_child?
end

Indeed, all the work here is done in the new'fix_parents_total'method, which is called if the check-box is clicked for an item which is not the parent node, i.e. does not have children rows. In the new 'fix_parents_total' helper method we obtain parent's iter (parent), and the iter for its first child (tmp_iter), which is subsequently advanced through the list of all children rows for this product category and in the process accumulating the total value for all the selected 'Count' cells identified by the GItm::QTY_INDEX. If a cell is selected is determined by the status of its 'Buy' column. Namely, when checked, the model's column identified by the GItm::BUY_INDEX contains value true. Finally, the parent's 'Count' value is set to the newly accumulated total value.

def fix_parents_total(iter)
  parent = iter.parent
  tmp_iter = parent.first_child
  total = tmp_iter[GItm::BUY_INDEX] ? tmp_iter[GItm::QTY_INDEX] : 0
  (total += tmp_iter[GItm::QTY_INDEX] if tmp_iter[GItm::BUY_INDEX]) while tmp_iter.next!
  parent[GItm::QTY_INDEX] = total
end

The other (i.e. the first) modification in the 'toggle-rndr-2.rb' example program is found in the area where the 'Buy' column is set up and is shown here:

column.set_cell_data_func(renderer) do |tvc, cell, model, iter|
  iter[GItm::BUY_INDEX] = any_child_set_to_buy(iter) if iter.has_child?
end

Here the helper method'any_child_set_to_buy'is used to inspect all the children whether any one's child has the check-box checked and accordingly request the modification of only the product category control rows, i.e. the parent nodes with children rows, so the control record (product category row) will convey the '/any checked/ or /all unchecked/' status of its children. We need this helper method to traverse all the children for the pertinent product category and return true if any child has 'Buy' value checked and false otherwise (indeed, all the children are traversed only if the latter is true):

def any_child_set_to_buy(parent)
  tmp_iter = parent.first_child
  return true if tmp_iter[GItm::BUY_INDEX]
  (return true if tmp_iter[GItm::BUY_INDEX]) while tmp_iter.next!
  return false
end

The interesting thing is that the cell data function block (in the second code segment, counting from here up) controls two things, namely it provides info to our check-box altering mechanism when children rows are checked or unchecked, as well as it prevents user from directly altering the check-box status on the control product category row.

Optimization Note

You should avoid excessive use of cell data function, especially when there are many rows in the model. For instance in our last program example (toggle-rndr-2.rb) above we use it on two cell renderers or columns, namely, for 'Buy' and 'Count' columns. (Method invocations are rather time consuming. So do not forget data cell function and its code block are invoked for each cell in all the columns for which they are defined. If we could manage to process both columns in one data cell function rather than two, that would eliminate the need for the second pass through all the rows and subsequent 'data cell function code block' invocation.) We could improve the performance of this program if we added as the last line the following to 'fix_parents_total' method:

parent[GItm::BUY_INDEX] = total == 0 ? false : true

This would eliminate the need for 'any_child_set_to_buy' method, and for the cell data function on 'Buy' column, and would decrease the CPU usage. The behaviour of the program would suffer a little, however, users would most likely tolerate it since any mouse movement over the tree view triggers a self-correcting action. For full implementation of this variant check out the example in section 8.7.4 Combo Box Renderers.

Last modified:2012/10/15 02:00:43
Keyword(s):
References:[tut-gtk2-treev] [tut-gtk2-treev-pxbr] [tut-gtk2-treev-cbbr] [tut-gtk] [tut-gtk2-treev-spbttr]