ListView Tutorial
VR::ListView offers you the abilitly to make complex listviews without having to struggle with all the details of GtkTreeView. You can create a VR::TreeView or VR::ListView with only a few lines of code and do almost anything with it. You can present any type of data you like including DateTime, VR::CalendarCol, VR::SpinCol, VR::ProgressCol, VR::ComboCol, and GdkPixbuf. You can even add your own user-defined objects, and edit them. You can create columns from instance variables, methods or any object. Everything will be editable and sortable.
Simple Example
In this example, we'll make a ListView that displays a list of folders. It's the same listview that visualruby uses when you click on the “Open Project” button.
When its finished it will look like this:
This listview has 3 columns of data in the model:
Folder Icon (GdkPixbuf)
Folder Name (String)
Modified Date (DateTime)
The first thing we need to do is give each model column an id so we can identify each column. So our code should begin with:
cols = Hash.new
cols[:project_folder] = { :pix => Gdk::Pixbuf, :file_name => String}
cols[:modified] => DateTime
@view = VR::ListView.new(hash)
This defines a hashtable for the columns in the listview where there are two visable columns, “Project Folder” and “Modified”. The “Project Folder” column will have two columns of data grouped together inside of it: a GdkPixbuf and a String. The “Modified” column will just conatin one data column for the date.
Every VR::ListView has an underlying data structure called a model. The model holds a table of raw data whereas the “view” has different columns display purposes. This constructor will create a data model from the cols hash with three columns in it: :pix, :file_name, and :modified. From now on, we will refer to each column using these symbols to identify both the data columns in the model and the visual columns in the view. All the methods in VR::ListView will require these IDs.
The symbol :project_folder is simply a name for the column header. It will be used to create the string “Project Folder”, then it will be discarded. You don't need to refer to it in the future.
The listview shows the data on the screen using renderers. These renderers take each individual cell of data and render it on the screen so you can see it. There are many different ways to display data, so there are many different types of renderers. The renderers are Gtk objects, and you can change how everything appears by setting their properties. Consult the Gtk docs to learn all the different properties you can utilize.
When a VR::ListView is contructed, a renderer is assigned for each column of data in the model based on the type (class) of the column. See VR::ListView#new for a list of the various types of data. For example a column that holds DateTime objects would have a VR::CellRendererDate renderer. The renderer controld how the data is diplayed on the screen.
At this point, our ListView is blank, it just has three columns defined in the model, and two colums in the view. Now its time to add some data to the model. To add data, we use the VR::ListView#add_row method. Now we create a method named refresh() to populate a VR::ListView:
def refresh()
@view.model.clear #search for settings file
Dir.glob(ENV["HOME"] + "/**/.vr_settings.yaml")).each do |fn|
mod = File.stat(fn).mtime #modified time
row = @view.add_row
row[:pix] = PIX
row[:file_name] = File.dirname(fn)
row[:modified] = DateTime.parse(mod.to_s)
end
end
The VR::ListView#add_row method returns a modified GtkTreeIter object which will accept our ID symbols.
Another way to do the same thing would be:
iter = @view.model.append
iter[id(:pix)] = PIX
etc.
Where the VR::ListView#id method provides the column number.
Subclassing VR::ListView
In the example, we created an instance variable, @view to hold our VR::ListView. A much cleaner approach is to subclass VR::ListView:
class ProjectChooserView < VR::ListView
PIX = Gdk::Pixbuf.new(File.dirname(__FILE__) + "/../../img/folder.png")
def initialize()
cols = Hash.new
cols[:project_folder] = { :pix => Gdk::Pixbuf, :file_name => String}
cols[:modified] => DateTime
super(cols)
end
def refresh()
@view.model.clear #search for settings file
Dir.glob(ENV["HOME"] + "/**/.vr_settings.yaml")).each do |fn|
mod = File.stat(fn).mtime #modified time
row = @view.add_row
row[:pix] = PIX
row[:file_name] = File.dirname(fn)
row[:modified] = DateTime.parse(mod.to_s)
end
end
end
Then make a subclass of it to handle the GUI components:
class ProjectChooserGUI < ProjectChooserView
include GladeGUI
#note: there is an empty scrolledwindow on our glade form that we fill:
def before_show()
@builder["scrolledwindowProjTree"].add_child(@builder, self)
refresh()
end
def self__row_activated
return row = selected_row()
open_project( row[:file_name] )
end
end
This technique keeps everything organized:
-
ProjectChooserView encapsulates everything for the VR::ListView
-
ProjectChooserGUI encapsulates all GUI components
And it looks like this:
Editing Cells
All the column types are editable except GdkPixbuf and VR::ProgressCol. You can set a column to be editable by setting its (Renderer)#editable property to true. The editable property set similarly to any other property:
ren_editable(:person => true) # makes :person col editable
ren_attr(:person, :age, :editable => true)
ren_editable(true) #set all columns to be editable
Many of the renderers default to be editable. How an object gets edited depends on the type of the object. For example, clicking on a VR::CalenderCol object makes a little calender appear on the screen so you can select a date and time; A DateTime object will use a GtkEntry to edit the date string, and it will insist that you enter a valid date; VR::SpinCol will display as a GtkSpinButton and only allow you to enter valid numbers; And a VR::TextCol will display a small text editor to allow you to enter long Strings.
There's a great example of editing a listview in the example projects entitled “listview.”
Post-Editing Callback
There are times you will want to update the model after an edit, and you can do that (or anything else) in a post-edit callback. You can set it on the VR::CellRenderer objects by using the VR::ListView#renderer method:
@view.renderer(:name).edit_callback = Proc.new { |model_sym, row, view |
...your code here
}
This block will be called after the cell has been edited. You can use this callback to update any of the columns using the “row” parameter. For example, if you had an object in a column, and you edited it, you could write code to update other columns using the newly edited object.
-
model_sym–the column id symbol for the column (here, it would be :name)
-
row–a GtkTreeIter iter that will respond to symbols i.e.
row[:name]
-
view–the VR::ListView or VR::TreeView obect parent of the renderer
If you want to know the current value of the cell, use
row[model_sym]
Validating User Input
You can validate that the user has edited a cell correctly by using various VR::CellRenderers' validate_blocks. These blocks will be called before the the data model is changed, and if the block evlauates as false, the edit will be rejected. This is the format for the validate_blocks:
@view.renderer(:name).validate_block = Proc.new { |text, model_sym, row, view|
text == "Chester"
}
This will ensure that only the name “Chester” can be entered into the name column.
This example is for a VR::CellRendererText type column. The first value passed to the block is always the value of the edited cell. In this case its “text” which is a String.
-
text–value of edited cell. may be number for other types of renderers.
-
model_sym–the column ID symbol (i.e. :name)
-
row–a GtkTreeIter that has the ability to respond to column IDs
-
view–the VR::ListView or VR::TreeView parent of this renderer.
Objects in Columns
With VR::ListView and VR::TreeView, you can create columns of any type, including classes you've written yourself. To create a VR::ListView of your own classes, simply pass their types to the constructor:
@view = VR::ListView.new(:person => PersonClass, :employer => EmployerClass)
This will create a VR::ListView with two columns. The first column will hold instances of PersonClass, and the second will hold instances of EmployerClass. PersonClass and EmployerClass must be classes that you've already written and have been “required” into your code. These classes can be anything including subclasses of ActiveRecordBase.
Once you've constructed your VR::ListView, you can add records normally:
row = @view.add_row
row[:person] = PersonClass.new("Henry", 25)
row[:employer] = EmployerClass.new("Google Inc.")
Here we're constructing new instances of each class, but usually you'll be working with existing objects that you want to show in a listview. Obviously, you must add objects of the proper type for each column.
Now a record is added with the :person column in the model containing an instance of PersonClass etc. Now the VR::ListView is responsible for showing your user-defined class PersonClass on the screen???
So what appears in the listview?
The listview will execute the to_s method on the instace of PersonClass, and that will display in the listview. Therefore, you should override the to_s method in all the classes you use in VR::ListView:
class PersonClass
def initialize(name, age)
@name = name
@age = age
end
def to_s
return "#{@name} (#{@age})"
end
end
Now the text “Henry (25)” will appear in the listview for our example record.
Editing Objects in a VR::ListView
There's really not much point to adding objects to a VR::ListView if you just plan to look at them. You could just use Strings instead. The real advantage of adding objects to a listview is that you can click on them and interact with them. This section focuses editing objects, but really, you're not limited to just editing them. The object will be running completely independently, so you can do anytihng you desire in the object's code.
The objects you add to a VR::ListView should be GUI objects that include the GladeGUI interface. (If you don't understand GladeGUI, see the basic tutorials) Objects that use GladeGUI can be shown in their own window by calling their show() method. So, really our PersonClass should look more like this:
class PersonClass
#this makes the class visual:
include GladeGUI
def initialize(name, age)
@name = name
@age = age
end
#this is what's shown in the VR::TreeView
def to_s
return "#{@name} (#{@age})"
end
end
Now when you double-click on a person's cell, that object's show() method will execute showing the object on the screen. It is likely that the PersonClass will show the person's name and age and allow you to edit each field. If you edit the object, your changes will be automatically reflected in the VR::ListView when you return. Also, if the VR::ListView is sorted, it will resort with the new value.
Altering Individual Cells' Appearance
You can change the appearance of an object in a listview by adding a method named, visual_attributes to it. This is very useful, for example, if you have a listview of account balances, and you want to show the negative balances in red. This can be accomplished easily by adding a method named visual_attributes. Here is an example where everyone over 50 years old will be displayed in red:
class PersonClass
include GladeGUI
def initialize(name, age)
@name = name
@age = age
end
def to_s
return "#{@name} (#{@age})"
end
def visual_attributes
return @age > 50 ? {:background => "red" } : {:background => "white" }
end
end
Now, all PersonClass objects will display red backgrounds for people over 50 when they appear in a VR::ListView.
There's an example of this in the example project, “active_record2.”
Objects as Rows
In the previous example, Ruby objects were added to a VR::ListView in a single column. The object itself occupied the data column in the model, and the object's to_s method rendered the string to display in the listview.
You can also populate multiple columns of data from a single object using the load_object() method. This method will search the names of the columns, and try to match to instance variables and methods of the object. For example, if we used our PersonClass from the previous example, we could load the fields of the listview like this:
@view = VR::ListView.new(:name => String, :age => String, :to_s => String)
person = PersonClass.new("Henry", 25)
row = @view.add_row
row.load_object(person)
puts row[:name] # "Henry"
This would populate the :name and :age columns because the load_object() method would look at the column ID symbol for each column, and compare it to the instance variables of the person object. When it found a match (:name) it will fill-in the :name column with the value of the instance variable @name from the object.
Notice also that there is a column in the listview named :to_s. This column will be filled-in using the PersonClass#to_s method, as described in the next section.
Adding Methods to a ListView
You're not limited to populating columns with simple types of data like :name and :age. You can also populate a column of data using the output of a method. In the last example, one of the column ID symbols was :to_s. This column will match to the PersonClass#to_s method, so when you look at the VR::ListView on the screen, each cell will will display the output of each object's to_s method.
In this example, the to_s method will output a simple string to display on the screen, but you can make your object's method output any type of data for the listview. For example you could write a method called my_birthday():
class PersonClass
include GladeGUI
def initialize(name, age)
@name = name
@age = age
end
def my_birthday()
year = DateTime.now.year - age
return CalendarCol.new(year, 01, 01)
end
end
The my_birthday() method will return a VR::CalendarCol object, which is a great way to edit a birthday date. The VR::ListView's constructor will need to reflect that there will be a column for the person's birthday:
@view = VR::ListView.new(:name => String, :age => Integer, :my_birthday => VR::CalenderCol)
row = @view.add_row
person = PersonClass.new("Henry", 25)
row.load_object(person)
puts row[:my_birthday] # "1987-01-01"
This will display the person's name, age and birthday in three columns.
When you click on a person's birthday a VR::CalendarCol object will appear so you can edit the birthday:
Notice that the column name, :my_birthday must match the method name, my_birthday() for the load_object() method to work.
Altering The View's Appearance
Making Columns Invisible
To make columns invisible, you set the GtkTreeViewColumn#visible property. Set the visible propery in the same way you set any property in a VR::ListView:
col_visble(:person => false) # person is now invisible
col_visible(true) # all cols visible
Note: cell renderers also have a property named visible. But it just makes the data invisible and leaves the header in place. It really isn't very useful. Just remmeber to use the VR::ListView#col_visible method.
Setting New Titles
VR::ListView and VR::TreeView will automatically make nice-looking titles based on the IDs of the columns. But sometimes you may want to change the columns' titles for the sake of appearance. You can change any column's title my setting the GtkTreeViewColumn#title property:
col_title(:person => "Name (age)", :employer => "Place of Work")
Setting the Width of Columns
You can set any column to have a fixed width by setting the GtkTreeViewColumn#width property.
col_width(:person => 200)
This will also set the GtkTreeViewColumn#sizing property to Gtk::TreeViewColumn::FIXED. Which makes the column a fixed width type instead of “auto.”
You can also set make all the widths equal:
col_width(200)
Setting the Alignment of Columns
If you want to right-justify, left-justify, or center-justify a column,
you can set the alignment of columns using the GtkTreeViewColumn#xalign and GtkCellRendererText#xalign methods. The column version will set the alignment of the text in the header, and the renderer version will align the text in the cells. It can be set to any Float number ranging from 0 to 1. 0.00 = left justify, 1.00 = right justify
@view.ren_xalign(:modified => 0.5) # center justify text in cells
@view.col_xalign(:modified => 0.5) # center justify text in header
This is one of the rare circumstances where the ren_<property> and col_<propery> methods differ because both the renderer and column have an identical property, xalign.
Sorting Columns
Its easy to sort colums in a VR::ListView. Visual ruby adds a “sortable” property to the columns that you can set like any other property:
@view.col_sortable(:first_name => true, :last_name => true)
You can also make all the columns sortable by just passing one value:
@view.col_sortable(true)
If you'd like to have one column sort based on another column's value, you can set the “sort_column_id” property. This could be useful, for example, if you wanted a column with peoples' full names to sort based on a last_name column:
@view.col_sort_column_id(:full_name => id(:last_name))
Now when you click on the full_name column, the names will sort in last_name order.
The “sort_column_id” method requires that you pass an Integer to identify the column number. You must pass the number of the column to this method, so you can use the VR::ListView#id method to convert the id symbol into the column number. The code looks like this:
@view.set_sort_column_id(:modified => id(:modified), :file_name => id(:modified))
or equivalently:
@view.column(:modified).sort_column_id = id(:modified)
@view.column(:file_name).sort_column_id = id(:modified)
This code will make the column headers clickable. When a user clicks on the header of a column it will sort on the column number provided. So, when the user clicks on the “Modified” header, the VR::ListView will sort in order of the :modified column. Also, notice that if the user clicks on the “File Name” header, it will sort according to the :modified column. You can make headers sort on any column you like. This is a very useful feature because you may want to define an extra column in your model just for sorting purposes.
You can see this sorting in action, by clicking on the “Open Project” button in visualruby. Try clicking on the headers.
Important note about sorting:
Sometimes you get errors when you try to add records to a listview when there is an active sort_column_id. I know errors occur when you try to add records when the the listview is sorted on a DateTime column. You should re-set the active sort_column_id to a “safe” column type before adding records. Simply, use this type of code:
@view.model.set_sort_column_id(0)
This assumes that the first column in your model is a String or Fixnum (not a DateTime or UserDefinedClass!)
Adding ActiveRecord Objects
Under construction.
Working with Gtk
There may be times where you want to do something very customized to your VR::ListView, and you may need to use Gtk to do it.
VR::ListView (and VR::TreeView) are subclasses of Gtk::TreeView, so you can always program them exactly like a GtkTreeView. In fact, all the classes used by VR::ListView and VR::TreeView are directly subclassed from Gtk:
VR::TreeView < GtkTreeView
VR::ListView < GtkTreeView
VR::TreeViewColumn < GtkTreeViewColumn
VR::CellRendererText < GtkCellRendererText
VR::CellRendererCombo < GtkCellRendererCombo
VR::CellRendererToggle < GtkCellRendererToggle
VR::CellRendererSpin < GtkCellRendererSpin
VR::CellRendererProgress < GtkCellRendererProgress
VR::CellRendererPixbuf < GtkCellRendererPixbuf
If you read the documentation for all the superclasses you'll find that you can do almost anything using Gtk's methods (but its A LOT of work!) That's why visualruby was created.
Often, these methods use GtkTreeIters to refer to rows of data in the model. In order to use these their iters, you need to know the column number in the model, as in this example:
iter = selection.seleted
iter[3] = "Chester"
To get the column number “3”, you'll need to convert the symbol for the column to an integer using the VR::ListView#id method:
iter = selection.seleted
iter[id(:name)] = "Chester"
or you can convert the iter to accept column IDs using the VR::ListView#vr_row method:
row = vr_row(selection.seleted)
iter[:name] = "Chester"
Referencing Renderers and Columns
Many of Gtk's methods use their cell renderers and GtkTreeViewColumns. VR makes it easy to get a reference to any renderer or column using the VR::ListView#renderer and VR::ListView#column methods:
@view.renderer(:name)
@view.column(:name)
Both of these methods will work on all VR::TreeView and VR::ListView objects.