The Qt model/view APIs are used throughout Qt — in Qt Widgets, in Qt Quick, as well as in other non-GUI code. As I tell my students when I deliver Qt trainings: mastering the usage of model/view classes and functions is mandatory knowledge, any non-trivial Qt application is going to be data-driven, with the data coming from a model class.
In this blog series I will show some of the improvements to the model/view API that KDAB developed for Qt 5.11. A small word of advice: these posts are not meant to be a general introduction to Qt’s model/view (the book’s margin is too narrow… but if you’re looking for that, I suggest you start here) and assumes a certain knowledge of the APIs used.
Implementing a model class
Data models in Qt are implemented by QAbstractItemModel subclasses. Application developers can either choose one of the ready-to-use item-based models coming with Qt (like QStringListModel or QStandardItemModel), or can develop custom model classes. Typically the choice falls on the latter, as custom models provide the maximum flexibility (e.g. custom storage, custom update policies, etc.) and the biggest performance. In my experience with Qt, I have implemented probably hundreds of custom models.
For simplicity, let’s assume we are implementing a table-based model. For this use case, Qt offers the convenience QAbstractTableModel class, which is much simpler to use than the fully-fledged QAbstractItemModel. A typical table model may look like this:
class TableModel : public QAbstractTableModel
{
public:
explicit TableModel(QObject *parent = nullptr)
: QAbstractTableModel(parent)
{
}
// Basic QAbstractTableModel API
int rowCount(const QModelIndex &parent) const override
{
return m_data.rowCount();
}
int columnCount(const QModelIndex &parent) const override
{
return m_data.columnCount();
}
QVariant data(const QModelIndex &index, int role) const override
{
if (role != Qt::DisplayRole)
return {};
return m_data.getData(index.row(), index.column());
}
private:
Storage m_data;
};
First and foremost, note that this model is not storing the data; it’s acting as an adaptor between the real data storage (represented by the Storage class) and the views.
When used into a Qt view (for instance a QTreeView), this code works perfectly and shows us a nice table full of data, for instance like this:
![]()
Making the code more robust
The code of the class above has a few issues.
The first issue is that the implementation of rowCount() and columnCount() is, generally speaking, wrong. Those functions are supposed to be callable for every model index belonging to this model, plus the root (invalid) model index; the parameter of the functions is indeed the parent index for which we’re asking the row count / column count respectively.
When called with the root index, the functions return the right amount of rows and columns. However, there are no rows and no columns below any of elements in the table (because it is a table). The existing implementation does not make this distinction, and happily returns a wrong amount of rows/columns below the elements themselves, instead of 0. The lesson here is that we must not ignore the parent argument, and handle it in our rowCount and columnCount overrides.
Therefore, a more correct implementation would look like this:
int rowCount(const QModelIndex &parent) const override
{
if (parent.isValid())
return 0;
return m_data.rowCount();
}
int columnCount(const QModelIndex &parent) const override
{
if (parent.isValid())
return 0;
return m_data.columnCount();
}
The second issue is not strictly a bug, but still a possible cause of concern: we don’t validate any of the indices passed to the model’s functions. For instance, we do not check that data() receives an index which is valid (i.e. isValid() returns true), belonging to this very model (i.e. model() returns this), and pointing to an existing item (i.e. its row and column are in a valid range).
QVariant data(const QModelIndex &index, int role) const override
{
if (role != Qt::DisplayRole)
return {};
// what happens here if index is not valid, or not belonging to this model, etc.?
return m_data.getData(index.row(), index.column());
}
I personally maintain quite a strong point of view about this issue: passing such indices is a violation of the API contract. A model should never be assumed to be able to handle illegal indices. In other words, in my (not so humble) opinion, the QAbstractItemModel API has a narrow contract.
Luckily, Qt’s own views and proxy models honour this practice. (However, be aware that some other bits of code, such as the old model tester from Qt Labs, does not honour it, and will pass invalid indices. I will elaborate more on this in the next blog post.)
Since Qt will never pass illegal indices to a model, it’s generally pointless to make QAbstractItemModel APIs have wide contracts by handling all the possible inputs to its functions; this will just add unnecessary overhead to functions which are easily hotspots in our GUI.
On the other hand, there are cases in which it is desirable to have a few extra safety checks in place, in the eventuality that an illegal index gets passed to our model. This can happen in a number of ways, for instance:
- in case we are developing a custom view or some other component that uses our model via the model/view API, accidentally using wrong indices;
- a QModelIndex is accidentally stored across model modifications and then used to access the model (a QPersistentModelIndex should have been used instead);
- the model is used in combination with one or more proxy models, which may have bugs in the mapping of the indices (from source indices to proxy indices and viceversa), resulting in the accidental passing of a proxy index to our model’s functions.
In the above scenarios, a bug somewhere in the stack may cause our model’s methods to be called with illegal indices. Rather than crashing or producing invalid data, it would be very useful to catch the mistakes, in order to gracefully fail and especially in order to be able to debug them.
In practice all of this means that our implementation of the QAbstractItemModel functions needs some more thorough checks. For instance, we can rewrite data() like this:
QVariant data(const QModelIndex &index, int role) const override
{
// index is valid
Q_ASSERT(index.isValid());
// index is right below the root
Q_ASSERT(!index.parent().isValid());
// index is for this model
Q_ASSERT(index.model() == this);
// the row is legal
Q_ASSERT(index.row() >= 0);
Q_ASSERT(index.row() < rowCount(index.parent()));
// the column is legal
Q_ASSERT(index.column() >= 0);
Q_ASSERT(index.column() < columnCount(index.parent()));
if (role != Qt::DisplayRole)
return {};
return m_data.getData(index.row(), index.column());
}
Instead of hard assertions, we could use soft assertions, logging, etc. and returning an empty QVariant. Also, do note that some of the checks could (and should) also be added to the rowCount() and columnCount() functions, for instance checking that if the index is valid then it indeed belongs to this model.
Introducing checkIndex
After years of developing models I’ve realized that I must have written some variation of the above checks countless times, in each and every function of the QAbstractItemModel API. Recently I gave the question some more thought, and I came up with a solution: centralize the above checks, so that I don’t have to re-write them every time.
In Qt 5.11 I have added a new function to QAbstractItemModel: QAbstractItemModel::checkIndex(). This function takes a model index to check, and an option to determine the kind of checks that should be done on the index (see the function documentation for all the details).
In case of failure, the function returns false and prints some information in the qt.core.qabstractitemmodel.checkindexlogging category. This gives us the flexibility of deciding what can be done on failure, and also to extract interesting data to debug an issue.
Using the brand new checkIndex() our data() reimplementation can now be simplified to this:
QVariant data(const QModelIndex &index, int role) const override
{
// data wants a valid index; moreover, this is a table, so the index must not have a parent
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
if (role != Qt::DisplayRole)
return {};
return m_data.getData(index.row(), index.column());
}
Again, the example has an hard assert, which means that the program will crash in case of an illegal index (forcing the developer to do something about it). On the other hand the check will disappear in a release build, so that we don’t pay the price of the check at each invocation of data(). One could instead use a soft assert or just a plain if statement (as many models — unfortunately — do, including the ones coming with Qt) for customizing the outcome of the check.
This is an example of the logging output we automatically get in case we pass an invalid model index, which is not accepted by data():
qt.core.qabstractitemmodel.checkindex: Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)
And this is an example of the output in case we accidentally pass an index belonging to another model (which happens all the time when developing custom proxy models):
qt.core.qabstractitemmodel.checkindex: Index QModelIndex(0,0,0x0,ProxyModel(0x7ffee145b640)) is for model ProxyModel(0x7ffee145b640) which is different from this model TableModel(0x7ffee145b660)
Conclusions
I hope that this addition to QAbstractItemModel will help developers build better data models, and to quickly and effectively debug situations where the model API is being misused.
In the next instalment I will talk about other improvements to the model/view framework in Qt 5.11.
About KDAB
KDAB is a consulting company offering a wide variety of expert services in Qt, C++ and 3D/OpenGL and providing training courses in:
KDAB believes that it is critical for our business to contribute to the Qt framework and C++ thinking, to keep pushing these technologies forward to ensure they remain competitive.
The post New in Qt 5.11: improvements to the model/view APIs (part 1) appeared first on KDAB.