FEB
8
2007

Using QtRuby with Rails ActiveRecord based Qt::AbstractTableModels

I do Ruby Rails development as part of my day job, and one of the nice parts of Rails is the ActiveRecord Object-Relational Mapping framework. Today I've been playing with QtRuby using a Qt::AbstractTableModel based on ActiveRecord, and it's really simple to implement and works really well.

To use ActiveRecord without Rails you create a database.yml file with the database details like this for instance:

development:
  adapter: postgresql
  database: boat_development
  username: rdale
  password: XXXXX

I googled a bit for how to connect to the database and found this code. You read in the yaml and connect to the database (I needed to pass a '-I/usr/share/rails/activerecord/lib' to ruby to find the active record lib):

def setupDatabase
    @dbs = YAML::load(ERB.new(IO.read("database.yml")).result)

    # to slurp records into production db, change this line to production.
    curr_db = @dbs["development"]

    # these parameters are mysql-specific, figure out how to improve
    ActiveRecord::Base.establish_connection(:adapter => curr_db["adapter"], 
                                            :database => curr_db["database"],
                                            :host => curr_db["host"], 
                                            :username => curr_db["username"], 
                                            :password => curr_db["password"])
end

I have a simple table called 'boats' for this example, and I set it up with a Rails Migration:

class BoatsTable < ActiveRecord::Migration
  def self.up
    create_table :boats do |t|
      t.column :name, :string, :limit => 20
      t.column :capacity, :integer
      t.column :state, :integer			# 0: funciona, 1: no funciona
    end
    garajonay = Boat.create(:name=> "Garajonay", :capacity => 268, :state => 0)
    orone = Boat.create(:name=> "Orone", :capacity => 268, :state => 0)
  end

  def self.down
    drop_table :boats
  end
end

So you need to run a 'rake migrate' command to create the table from the migration. Then you can access it with a simple model like this:

class Boat < ActiveRecord::Base
end

app = Qt::Application.new(ARGV)
setupDatabase()
boats = Boat.find(:all)
model = BoatTableModel.new(boats)
table = Qt::TableView.new
table.model = model
table.show
app.exec

Defining a custom Qt::AbstractTableModel is very simple. I based this on some code from Hans Fugal who has written a QtRuby based app to maintain a recipe database.

class BoatTableModel < Qt::AbstractTableModel
    def initialize(boats)
        super()
        @boats = boats
    end

    def rowCount(parent)
        @boats.size
    end

    def columnCount(parent)
        3
    end

    def data(index, role=Qt::DisplayRole)
        invalid = Qt::Variant.new
        return invalid unless role == Qt::DisplayRole or role == Qt::EditRole
        boat = @boats[index.row]
        return invalid if boat.nil?

        v = case index.column
        when 0
          boat.name
        when 1
          boat.capacity
        when 2
          boat.state
        else 
          raise "invalid column #{index.column}"
        end || ""
        return Qt::Variant.new(v)
    end

    def headerData(section, orientation, role=Qt::DisplayRole)
        invalid = Qt::Variant.new
        return invalid unless role == Qt::DisplayRole

        v = case orientation
        when Qt::Horizontal
          ["Name","Capacity","State"][section]
        else
          ""
        end
        return Qt::Variant.new(v)
    end

    def flags(index)
        return Qt::ItemIsEditable | super(index)
    end

    def setData(index, variant, role=Qt::EditRole)
        if index.valid? and role == Qt::EditRole
            s = variant.toString
            boat = @boats[index.row]
            case index.column
            when 0
                boat.name = s
            when 1
                boat.capacity = s.to_i
            when 2
                boat.state = s.to_i
            else
                raise "invalid column #{index.column}"
            end
            boat.save

            emit dataChanged(index, index)
            return true
        else
            return false
        end
    end
end

And that's all there is to it. You can edit values in the table and the database is updated. And it should be very easy to create general purpose tools for doing CRUD stuff that wouldn't need to know anything about the database table, as they can get the column names and numbers of columns from ActiveRecord.