A common data model involves lists, tables and trees of more or less uniform items. Several standard widgets provided by the library use such a model. The abstract WAbstractItemModel base class provides the interface which is used by these view classes.
Not only Table Views and Tree Views display (and interact with) data from an item model, but so do also the list-oriented widgets such as Combo box, Selection box, Auto complete, and charting widgets such as Cartesian Chart, Scatter Plot and Pie Chart.
An item model is essentially a table of items, where each item can be a parent to a nested table of items.
Since recursive statements like the one above can confuse even a seasoned programmer, let's start from a simple model, and extend it to more complex instances.
In its most simple and perhaps most common form, an item model simply stores a list of items. Such a model has only one column.
Each item may hold different facets of the data, stored as different Item Data Roles. If a table has two dimensions (rows and columns), then data roles could be considered as a third dimension.
The common use-case for item data roles is that for a single item, there may be a textual representation, but also an icon, a customized style class, a link, etc...
The built-in views will thus interpret subsets of this data to render a single item. In particular, the following data roles are commonly supported:
A tree model is - like a list - a model with a single column. In addition, each item may be the parent of another list.
At this point it becomes necessary to introduce the concept of a WModelIndex to uniquely identify an item. A model index is a data structure containing:
The recursion is thus achieved by associating a parent index
with each item index. By convention, top-level items have an
Invalid index (which is a default constructed
WModelIndex
). To make the recursive definition
consistent, one can also imagine an invisible root item
(corresponding to the "invalid" index) which is the parent of
the top-level items.
Finally, a tree table extends the list model by allow additional columns of data to be associated with each item row.
None of the standard Views render hierarchical data that is not present in the first column ! While such data structures can indeed be defined by item models, this will effectively be ignored by the standard View classes.
To get you going, and more than sufficient for simple needs, the library provides a number of standard and generic models, which store the data in memory.
WStandardItem
items.Separating models from views would be not very useful, if it were not of the ability to implement customized models. These could be models that compute some or all data on the fly, or fetch this information from an underlying database or file system, or simply display information from an existing data structure in a tabular/tree-like way.
As a minimum, a custom table model should reimplement the following methods from WAbstractTableModel:
As an example of a custom table model, consider the following (nonsensical) model that simply displays row/column information for each item.
A custom tree model involves considerably more work. Each
internal item (in the first column) which has children, needs to
be identified by a unique 64-bit value (which may thus be a
long long
or a void *
pointer). Depending on
the source data, a suitable choice must be made for this data.
The following methods must be implemented for a minimally compliant hierarchical model:
As an example of a tree table model, consider the following model that loads information from a git repository (in this case, JWt's git). Only a minimum of information is kept in memory: we allocate a data structure only for folders that are being expanded, for use as internal pointer data in model indexes.
class GitModel extends WAbstractItemModel {
private static Logger logger = LoggerFactory.getLogger(GitModel.class);
public static ItemDataRole ContentsRole = ItemDataRole.of(ItemDataRole.User.getValue() + 1);
GitModel(final String repository) {
super();
this.git_ = new Git();
this.treeData_ = new ArrayList<GitModel.Tree>();
this.childPointer_ = new HashMap<GitModel.ChildIndex, Integer>();
this.git_.setRepositoryPath(repository);
this.loadRevision("master");
}
void loadRevision(final String revName) {
Git.ObjectId treeRoot = this.git_.getCommitTree(revName);
this.layoutAboutToBeChanged().trigger();
this.treeData_.clear();
this.childPointer_.clear();
this.treeData_.add(new GitModel.Tree(-1, -1, treeRoot, this.git_.treeSize(treeRoot)));
this.layoutChanged().trigger();
}
WModelIndex getParent(final WModelIndex index) {
if (!(index != null) || index.getInternalId() == 0) {
return null;
} else {
final GitModel.Tree item = this.treeData_.get(index.getInternalId());
return this.createIndex(item.getIndex(), 0, item.getParentId());
}
}
WModelIndex getIndex(int row, int column, final WModelIndex parent) {
int parentId;
if (!(parent != null)) {
parentId = 0;
} else {
int grandParentId = parent.getInternalId();
parentId = this.getTreeId(grandParentId, parent.getRow());
}
return this.createIndex(row, column, parentId);
}
int getColumnCount(final WModelIndex parent) {
return 2;
}
int getRowCount(final WModelIndex parent) {
int treeId;
if ((parent != null)) {
if (parent.getColumn() != 0) {
return 0;
}
Git.Object o = this.getObject(parent);
if (o.type == Git.ObjectType.Tree) {
treeId = this.getTreeId(parent.getInternalId(), parent.getRow());
} else {
return 0;
}
} else {
treeId = 0;
}
return this.treeData_.get(treeId).getRowCount();
}
Object getData(final WModelIndex index, ItemDataRole role) {
if (!(index != null)) {
return null;
}
Git.Object object = this.getObject(index);
switch (index.getColumn()) {
case 0:
if (role.equals(ItemDataRole.Display)) {
if (object.type == Git.ObjectType.Tree) {
return object.name + '/';
} else {
return object.name;
}
} else {
if (role.equals(ItemDataRole.Decoration)) {
if (object.type == Git.ObjectType.Blob) {
return "icons/git-blob.png";
} else {
if (object.type == Git.ObjectType.Tree) {
return "icons/git-tree.png";
}
}
} else {
if (role.equals(ContentsRole)) {
if (object.type == Git.ObjectType.Blob) {
return this.git_.catFile(object.id);
}
}
}
}
break;
case 1:
if (role.equals(ItemDataRole.Display)) {
if (object.type == Git.ObjectType.Tree) {
return "Folder";
} else {
String suffix = getSuffix(object.name);
if (suffix.equals("C") || suffix.equals("cpp")) {
return "C++ Source";
} else {
if (suffix.equals("h") || suffix.equals("") && !this.topLevel(index)) {
return "C++ Header";
} else {
if (suffix.equals("css")) {
return "CSS Stylesheet";
} else {
if (suffix.equals("js")) {
return "JavaScript Source";
} else {
if (suffix.equals("md")) {
return "Markdown";
} else {
if (suffix.equals("png") || suffix.equals("gif")) {
return "Image";
} else {
if (suffix.equals("txt")) {
return "Text";
} else {
return null;
}
}
}
}
}
}
}
}
}
}
return null;
}
Object getHeaderData(int section, Orientation orientation, ItemDataRole role) {
if (orientation == Orientation.Horizontal && role.equals(ItemDataRole.Display)) {
switch (section) {
case 0:
return "File";
case 1:
return "Type";
default:
return null;
}
} else {
return null;
}
}
private Git git_;
static class ChildIndex {
private static Logger logger = LoggerFactory.getLogger(ChildIndex.class);
public int parentId;
public int index;
ChildIndex(int aParent, int anIndex) {
this.parentId = aParent;
this.index = anIndex;
}
boolean equals(Object o) {
GitModel.ChildIndex other = ((ChildIndex) o);
return this.parentId == other.parentId && this.index == other.index;
}
int hashCode() {
int hash = 1;
hash = hash * 31 + this.parentId;
hash = hash * 31 + this.index;
return hash;
}
}
static class Tree {
private static Logger logger = LoggerFactory.getLogger(Tree.class);
Tree(int parentId, int index, final Git.ObjectId object, int rowCount) {
this.index_ = new GitModel.ChildIndex(parentId, index);
this.treeObject_ = object;
this.rowCount_ = rowCount;
}
int getParentId() {
return this.index_.parentId;
}
int getIndex() {
return this.index_.index;
}
Git.ObjectId getTreeObject() {
return this.treeObject_;
}
int getRowCount() {
return this.rowCount_;
}
private GitModel.ChildIndex index_;
private Git.ObjectId treeObject_;
private int rowCount_;
}
private List<GitModel.Tree> treeData_;
private Map<GitModel.ChildIndex, Integer> childPointer_;
int getTreeId(int parentId, int childIndex) {
GitModel.ChildIndex index = new GitModel.ChildIndex(parentId, childIndex);
Integer i = this.childPointer_.get(index);
if (i == null) {
final GitModel.Tree parentItem = this.treeData_.get(parentId);
Git.Object o = this.git_.treeGetObject(parentItem.getTreeObject(), childIndex);
this.treeData_.add(new GitModel.Tree(parentId, childIndex, o.id, this.git_.treeSize(o.id)));
int result = this.treeData_.size() - 1;
this.childPointer_.put(index, result);
return result;
} else {
return i;
}
}
Git.Object getObject(final WModelIndex index) {
int parentId = index.getInternalId();
final GitModel.Tree parentItem = this.treeData_.get(parentId);
return this.git_.treeGetObject(parentItem.getTreeObject(), index.getRow());
}
static String getSuffix(final String fileName) {
int dot = fileName.lastIndexOf('.');
if (dot == -1) {
return "";
} else {
return fileName.substring(dot + 1);
}
}
boolean topLevel(final WModelIndex index) {
return !(this.getParent(index) != null);
}
}
A model may support sorting by one of its columns. This sorting can be implemented within the model itself.
Sorting may be bolted onto a source model using the WSortFilterProxyModel, which is one of the standard proxy models.
A model does not necessarily need to be a static data source, but its data can also change, and data (rows/columns) can be added or removed. A model needs to generate events to inform Views of these modifications (for the events to which a View is subscribed). When implementing a custom model which is dynamic in nature, it is therefore important to emit these signals when making the modifications.
The model API also provides a standard interface to perform editing of the data, and some Views (such as the Tree View and Table Views) can be configured to allow editing of the data.
If a custom wants to support this editing API, it needs to reimplement the following methods from WAbstractTableModel:
EditRole
for the data used in editing