Thursday, March 3, 2011

Ruby, Qt4, and AbstractItemModel

There are no good examples of using QAbstractItemModel in Ruby. For most purposes, QStandardItemModel will suffice. In this particular case, which calls for a lazily-loaded tree, QStandardItemModel will not cut it.

What follows is a simple implementation. The number of columns is limited to 1, items are read-only, there is no DnD support, and the model only supports the Display role for item data. Needless to say, these features are easy enough to implement, and would only distract from the example of subclassing Qt::AbstractItemModel.

The Model

=begin rdoc
A basic tree model. The contents of all tree nodes are determined by the ModelItems, not the Model, so they may be loaded lazily.
=end
class Model < Qt::AbstractItemModel
  signals 'dataChanged(const QModelIndex &, const QModelIndex &)'

=begin rdoc
The invisible root item in the tree.
=end
  attr_reader :root

  def initialize(parent=nil)
    super
    @root = nil
  end

=begin rdoc
Load data into Model. This just creates a few fake items as an example. A full implementation would create and fill the top-level items after creating.
Note: @root is created here in order to make clearing easy. A clear() method just needs to set root to ModelItem.new('').
=end
  def load()
    @root = ModelItem.new('',nil)
    ModelItem.new('First', @root)
    ModelItem.new('Second', @root)
  end

=begin rdoc
This treats an invalid index (returned by Qt::ModelIndex.new) as the index of @root.
All other indexes have the item itself stored in the 'internalPointer' field.

See AbstractItemModel#createIndex.
=end
  def itemFromIndex(index)
    return @root if not index.valid?
    index.internalPointer
  end
 
=begin rdoc
Return the index of the parent item for 'index'.
The key here is to treat the invalid index (returned by Qt::ModelIndex.new) as the index of @root. All other (valid) indexes are generated by AbstractItemModel#createIndex. Note that the item itself is passed as the third parameter (internalPointer) to createIndex.

See ModelItem#parent and ModelItem#childRow.
=end
  def index(row, column=0, parent=Qt::ModelIndex.new)
    item = itemFromIndex(parent)
    if item
      child = item.child(row)
      return createIndex(row, column, child) if child
    end
    Qt::ModelIndex.new
  end

=begin rdoc
Return the index of the parent item for 'index'.
This is made a bit complicated by the fact that the ModelIndex must be created by AbstractItemModel.

The parent of the parent is used to obtain the 'row' of the parent. If the parent is root, the invalid Modelndex is used as usual.
=end
  def parent(index)
    return Qt::ModelIndex.new if not index.valid?

    item = itemFromIndex(index)
    parent = item.parent
    return Qt::ModelIndex.new if parent == @root

    pparent = parent.parent
    return Qt::ModelIndex.new if not pparent

    createIndex(pparent.childRow(parent), 0, parent)
  end

=begin rdoc
Return data for ModelItem. This only handles the case where Display Data (the text in the Tree) is requested.
=end
  def data(index, role)
    return Qt::Variant.new if (not index.valid?) or role != Qt::DisplayRole
    item = itemFromIndex(index)
    item ? item.data : Qt::Variant.new
  end

=begin rdoc
Set data in a ModelItem. This is just an example to show how the signal is emitted.
=end
  def data=(index, value, role)
    return false if (not index.valid?) or role != Qt::DisplayRole
    item = itemFromIndex(index)
    return false if not item
    item.data = value.to_s
    emit dataChanged(index, index)
    true
  end
    
  alias :setData :data=

=begin rdoc
Delegate rowCount to item.

See ModelItem#rowCount.
=end
  def rowCount(index)
    item = itemFromIndex(index)
    item ? item.rowCount : 0
  end

=begin rdoc
Only support 1 column
=end
  def columnCount(index)
    1
  end

=begin rdoc
All items can be enabled only.
=end
  def flags(index)
    Qt::ItemIsEnabled
  end

=begin rdoc
Don't supply any header data.
=end
  def headerData(section, orientation, role)
    Qt::Variant.new
  end
end

The ModelItem

=begin rdoc
An example of a ModelItem for use in the above Model. Note that it does not need to descend from QObject.

The ModelItem consists of a data member (the text displayed in the tree), a parent ModelItem, and an array of child ModelItems. This array corresponds directly to the Model 'rows' owned by this item.
=end
class ModelItem
  attr_accessor :data
  attr_accessor :parent
  attr_reader :children

  def initialize(data, parent=nil)
    @data = data
    @parent = parent
    @children = []
    parent.addChild(self) if parent
  end


=begin rdoc
Return the ModelItem at index 'row' in @children. This can be made lazy by using a data source (e.g. database, filesystem) instead of an array for @children.
=end
  def child(row)
    @children[row]
  end


=begin rdoc
Return row of child that matches 'item'. This can be made lazy by using a data source (e.g. database, filesystem) instead of an array for @children.
=end
  def childRow(item)
    @children.index(item)
  end


=begin rdoc
Return number of children. This can be made lazy by using a data source (e.g. database, filesystem) instead of an array for @children.
=end
  def rowCount
    @children.size
  end


=begin rdoc
Used to determine if the item is expandible.
=end
  def hasChildren
    childCount > 0
  end


=begin rdoc
Add a child to this ModelItem. This puts the item into @children.
=end
  def addChild(item)
    item.parent=self
    @children << item
  end
end

This should serve as a basic implementation of an AbstractItemModel.

Realistically, ModelItem would be subclassed to represent different types of items in the data source, each of which would also (likely) be subclassed from ModelItem. This allows a browsable tree to be created for navigating data hierarchies.

No comments:

Post a Comment