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, Wt'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.
#include <Wt/WAbstractItemModel.h>
#include "../../gitmodel/Git.h"
class GitModel : public Wt::WAbstractItemModel
{
public:
/*
* A custom role for the file contents of a Git BLOB object.
*/
static constexpr Wt::ItemDataRole ContentsRole = Wt::ItemDataRole::User + 1;
GitModel(const std::string& repository)
: WAbstractItemModel()
{
git_.setRepositoryPath(repository);
loadRevision("master");
}
void loadRevision(const std::string& revName) {
Git::ObjectId treeRoot = git_.getCommitTree(revName);
layoutAboutToBeChanged().emit(); // Invalidates model indexes
treeData_.clear();
childPointer_.clear();
/*
* This stores the tree root as treeData_[0]
*/
treeData_.push_back(Tree(-1, -1, treeRoot, git_.treeSize(treeRoot)));
layoutChanged().emit();
}
virtual Wt::WModelIndex parent(const Wt::WModelIndex& index) const {
if (!index.isValid() || index.internalId() == 0) {
return Wt::WModelIndex(); // treeData_[0] is the tree root
} else {
const Tree& item = treeData_[index.internalId()];
return createIndex(item.index(), 0, item.parentId());
}
}
virtual Wt::WModelIndex index(int row, int column,
const Wt::WModelIndex& parent = Wt::WModelIndex()) const {
int parentId;
if (!parent.isValid())
parentId = 0;
else {
int grandParentId = parent.internalId();
parentId = getTreeId(grandParentId, parent.row());
}
return createIndex(row, column, parentId);
}
virtual int columnCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const {
return 2;
}
virtual int rowCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const {
int treeId;
if (parent.isValid()) {
if (parent.column() != 0)
return 0;
Git::Object o = getObject(parent);
if (o.type == Git::Tree) { // is a folder
treeId = getTreeId(parent.internalId(), parent.row());
} else // is a file
return 0;
} else {
treeId = 0;
}
return treeData_[treeId].rowCount();
}
virtual Wt::cpp17::any data(const Wt::WModelIndex& index, Wt::ItemDataRole role = Wt::ItemDataRole::Display) const {
if (!index.isValid())
return Wt::cpp17::any();
Git::Object object = getObject(index);
switch (index.column()) {
case 0:
if (role == Wt::ItemDataRole::Display) {
if (object.type == Git::Tree)
return object.name + '/';
else
return object.name;
} else if (role == Wt::ItemDataRole::Decoration) {
if (object.type == Git::Blob)
return std::string("icons/git-blob.png");
else if (object.type == Git::Tree)
return std::string("icons/git-tree.png");
} else if (role == ContentsRole) {
if (object.type == Git::Blob)
return git_.catFile(object.id);
}
break;
case 1:
if (role == Wt::ItemDataRole::Display) {
if (object.type == Git::Tree)
return std::string("Folder");
else {
std::string suffix = getSuffix(object.name);
if (suffix == "C" || suffix == "cpp")
return std::string("C++ Source");
else if (suffix == "h" ||
(suffix == "" && !topLevel(index)))
return std::string("C++ Header");
else if (suffix == "css")
return std::string("CSS Stylesheet");
else if (suffix == "js")
return std::string("JavaScript Source");
else if (suffix == "md")
return std::string("Markdown");
else if (suffix == "png" || suffix == "gif")
return std::string("Image");
else if (suffix == "txt")
return std::string("Text");
else
return Wt::cpp17::any();
}
}
}
return Wt::cpp17::any();
}
virtual Wt::cpp17::any headerData(int section,
Wt::Orientation orientation = Wt::Orientation::Horizontal,
Wt::ItemDataRole role = Wt::ItemDataRole::Display) const {
if (orientation == Wt::Orientation::Horizontal && role == Wt::ItemDataRole::Display) {
switch (section) {
case 0:
return std::string("File");
case 1:
return std::string("Type");
default:
return Wt::cpp17::any();
}
} else
return Wt::cpp17::any();
}
private:
Git git_;
/*
* Identifies a folder given parent and index
*/
struct ChildIndex {
int parentId;
int index;
ChildIndex(int aParent, int anIndex)
: parentId(aParent), index(anIndex) { }
bool operator< (const ChildIndex& other) const {
if (parentId < other.parentId)
return true;
else if (parentId > other.parentId)
return false;
else return index < other.index;
}
bool equals(Wt::cpp17::any o) {
ChildIndex *other = Wt::cpp17::any_cast<ChildIndex *>(o);
return parentId == other->parentId &&
index == other->index;
}
int hashCode() {
int hash = 1;
hash = hash * 31 + parentId;
hash = hash * 31 + index;
return hash;
}
};
/*
* Data to be stored for an (expanded) folder
*/
class Tree {
public:
Tree(int parentId, int index, const Git::ObjectId& object, int rowCount)
: index_(parentId, index),
treeObject_(object),
rowCount_(rowCount)
{ }
int parentId() const { return index_.parentId; }
int index() const { return index_.index; }
const Git::ObjectId& treeObject() const { return treeObject_; }
int rowCount() const { return rowCount_; }
private:
ChildIndex index_;
Git::ObjectId treeObject_;
int rowCount_;
};
typedef std::map<ChildIndex, int> ChildPointerMap;
/*
* Expanded folder data
*/
mutable std::vector<Tree> treeData_;
/*
* Indexes into treeData_
*/
mutable ChildPointerMap childPointer_;
/*
* Gets or allocates an id for a folder.
*/
int getTreeId(int parentId, int childIndex) const {
ChildIndex index(parentId, childIndex);
ChildPointerMap::const_iterator i = childPointer_.find(index);
if (i == childPointer_.end()) {
const Tree& parentItem = treeData_[parentId];
Git::Object o = git_.treeGetObject(parentItem.treeObject(), childIndex);
treeData_.push_back(Tree(parentId, childIndex, o.id,
git_.treeSize(o.id)));
int result = treeData_.size() - 1;
childPointer_[index] = result;
return result;
} else
return i->second;
}
/*
* Gets the Git::Object that corresponds to an index.
*/
Git::Object getObject(const Wt::WModelIndex& index) const {
int parentId = index.internalId();
const Tree& parentItem = treeData_[parentId];
return git_.treeGetObject(parentItem.treeObject(), index.row());
}
static std::string getSuffix(const std::string& fileName) {
std::size_t dot = fileName.rfind('.');
if (dot == std::string::npos)
return "";
else
return fileName.substr(dot + 1);
}
bool topLevel(const Wt::WModelIndex& index) const {
return !parent(index).isValid();
}
};
constexpr Wt::ItemDataRole GitModel::ContentsRole;
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