Saturday, October 31, 2009

Displaying Multi Column Data in wxRuby

This post is about using the ListCtrl widget within wxRuby to display data in a table format, using several columns naming the fields in the data. The most straightforward way to manage the data is to construct an underlying data model, and use the virtual listctrl style to have your data displayed dynamically. This method also efficiently displays large datasets.

To demonstrate the simplest case, I will display some data from a CSV text file. For example, the
letter recognition dataset from the UCI machine learning repository consists of two files. The file ending '.data' contains the raw data. Each line contains the class and attributes of an instance from the data set. Each instance can be represented using a Ruby struct. Taking the attribute names from the file ending '.names', I build the following struct:

LetterInstance = Struct.new(:letter, :x_box, :y_box, :width, :height,
:on_pixels, :x_bar, :y_bar, :x2bar, :y2bar,
:xybar, :x2ybar, :xy2bar, :x_ege, :xegvy,
:y_ege, :yegvx)

I define the Struct with the attribute names in the order that they occur in the CSV file, as this makes reading the CSV file simple. To read in the textfile to create an array of instances of this struct:


@@data = []

File.open("letter-recognition.data", "r") do |f|
f.each_line do |line|
@@data << LetterInstance.new(*line.strip.split(","))
end
end

Displaying the Data

Building the list control is almost trivial. I use two facts about Ruby Structs to help in constructing the table automatically. First, the method 'members' can be used to find all the field names within the Struct; these are used for the column headings.

For example: @@data[0].members.join(", ")
returns: "letter, x_box, y_box, width, height, on_pixels, x_bar, y_bar, x2bar, y2bar, xybar, x2ybar, xy2bar, x_ege, xegvy, y_ege, yegvx"

Second, fields within a Struct can be accessed using array indexes. So, @@data[0][0] returns the value in the 'letter' field of the first item.

The following few lines of code create a class for our own virtual list control, which extends Wx::ListCtrl. We are building a particular kind of list control, which uses 'report format' and a 'virtual list data model'. Other kinds exist, but are not covered here. The initialize method builds the columns automatically, and sets the number of items.
The method 'on_get_item_text' is the one which makes this virtual list control so easy to use. The method is called only when an item is to be displayed. It is given the index of the item to display, and the column number. Your method just has to return the string for this item and column: as our data is stored as an array of structs, the implementation is as simple as can be.
# -- show data in a ListCtrl

require 'wx'

class LetterDataList < Wx::ListCtrl
def initialize parent
super(parent, :style => Wx::LC_REPORT | Wx::LC_VIRTUAL)
return if @@data.nil? or @@data.empty?

# Directly retrieve field names in Struct to populate table with columns
@@data[0].members.each_with_index do |name, index|
insert_column(index, name.to_s)
end

set_item_count @@data.size
end

# use array like indexing of fields in Struct to
# directly retrieve data for display
def on_get_item_text(item, column)
@@data[item][column]
end
end

class LetterDisplay < Wx::Frame
def initialize
super(nil, :title => "Letter Recognition Data")

LetterDataList.new self
end
end

Wx::App.run do
LetterDisplay.new.show
end


A screenshot of the result:


Handling Events


The list control supports a number of events. The most useful are for catching double clicks on a row or change in the highlighted row. Double clicks are known as activating the item, and single clicks as selecting the item. The event evt_list_item_activated captures a double click, and the event evt_list_item_selected captures a change in the focussed item (through a single click or use of arrow keys). Both events are easy to capture and redirect to a method to handle them. The following code will just print out the item(s) now selected by walking through the list of selected indices obtained by calling selections:

class LetterDataList < Wx::ListCtrl
def initialize parent
super(parent, :style => Wx::LC_REPORT | Wx::LC_VIRTUAL)
return if @@data.nil? or @@data.empty?

# Directly retrieve field names in Struct to populate table with columns
@@data[0].members.each_with_index do |name, index|
insert_column(index, name.to_s)
end

evt_list_item_selected(self) { selected_item }

set_item_count @@data.size
end

# use array like indexing of fields in Struct to
# directly retrieve data for display
def on_get_item_text(item, column)
@@data[item][column]
end

def selected_item
get_selections.each do |selection|
puts @@data[selection]
end
end

0 comments:

Post a Comment