Skip to content

New in Qt 5.11: improvements to the model/view APIs (part 1)

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.checkindex logging 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.

FacebookTwitterLinkedInEmail

Categories: C++ / KDAB Blogs / KDAB on Qt / Qt / QtDevelopment

14 thoughts on “New in Qt 5.11: improvements to the model/view APIs (part 1)”

  1. CheckIndexOption usage in the examples suggested that it is an enum class. I checked the source and it is indeed an enum class. However the linked Qt documentation lists enum values as if CheckIndexOption is just an enum.

  2. Thanks a lot Giuseppe!
    I’ve been using Qt for many years and still get tripped up with model/view issues fairly frequently, so any improvements here will be welcome. I rely on the old model tester to help catch issues – are you looking at updating it? Or are you trying to make it obsolete with new methods like checkIndex()?

    1. Giuseppe D'Angelo

      Hi Andy,
      (not sure what happened to my previous comment, trying to repost it now).
      checkIndex() and the model tester are supposed to complement each other. checkIndex() is “defensive programming” for your model (although I disagree, as I said, to me models have narrow contracts), but more importantly, it acts just at a local level — one particular function with one particular parameter. The model tester instead can do semantic checks, spanning across multiple functions. For instance, it can check that if the model says that there are rows/columns under an item, then asking for index(0, 0, item) returns a valid index. Or, it can check that the row count after a rowsInserted matches the advertised number of rows.
      Unfortunately, the existing model tester doesn’t work with checkIndex(), because it violates the narrow contract and deliberately supplies invalid indices to models. But I’ve something to say about that — in the next blog post!

  3. Which is the best way to update a QML a view when a model changes? What happens when the model changes in a different thread (very common situation). Thanks.

    1. Giuseppe D'Angelo

      Hi Gianluca,
      The “best” way is simply using the QAbstractItemModel APIs for triggering notifications on change (all the rows inserted / rows removed functions, dataChanged, and the like). Please refer to the model view documentation I linked above for more information.
      Regarding changes from a different thread, things are more complicated: a QAbstractItemModel is a QObject and I consider it non-reentrant. So you can only touch it from one thread (and if you use from QML, that thread is the GUI thread). This implies that any change from the data from another thread require some synchronization into your model, synchronization that must be completely invisible to the “users” of the model (aka the viewS). In other words: the views in the main thread must always have a coherent vision of the model; the model must take care of doing syncronizations internally.

  4. Instead of using all those asserts and pre-condition checks, I would think using an exception would be the right way to go. Exceptions allow you to avoid all those repeated checks (and any model will repeat them heavily) so that you don’t burn cpu on what should be 99.999% correct indexes.
    Another question is why/how would you ever get an invalid index? If you mention multithreading in your answer, then your asserts are useless anyway because they are not thread safe.

    1. Giuseppe D'Angelo

      Hi Jason,
      do you mean having checkIndex() throw an exception? Or having model functions (such as data(), etc.) throw exceptions?
      In general, exceptions can’t be used in Qt’s own code at all. Moreover, Qt doesn’t allow exceptions to bubble through itself, so throwing from a custom model of yours isn’t doable either.
      When you say “Exceptions allow you to avoid all those repeated checks” I’m not sure what you mean. The point I tried to make in the blog post is that such checks shouldn’t exist in the first place, so there’s nothing to do waste. However, just in case something goes wrong, it might be useful to have a way to debug the indexed passed through the models. This does not mean having wide contracts, but just a valuable debugging aid. (Or, if you prefer having wide contracts, you can have them too…). checkIndex aims to be that debugging aid, by centralizing those checks and avoiding lots of c&p code in model subclasses.
      About the “how would you ever get an invalid index” this can actually happen for many many reasons, I stated some in the blog post. For instance, one can build a proxy model that does some incorrect mapping and accidentally passes a proxy index to the source model, or one can develop a custom “view” that does a similar mistake.
      Hope I’ve clarified a few things here!

  5. Hi Giuseppe,
    thank you for this information. But I’m not sure how the data() method is working without using the “role” variable:
    QVariant data(const QModelIndex &index, int role) const override
    {
    if (role != Qt::DisplayRole)
    return {};
    return m_data.getData(index.row(), index.column());
    }
    If I debug in my qml application the role variable changes while index.column() remains at zero. So how can I increase the “column”?
    Thank you,
    Thomas

    1. Giuseppe D'Angelo

      Hi,
      This fundamentally depends on what kind of view you’re using. ListView, Repeater and similar views in Qt Quick only use column 0 (they’re list-based views) and different roles to extract different pieces of information for each row. TableView (in Qt Quick) or QTableView/QTreeView would also be able to use extra columns.

      1. Thank you. I’m using the (old) Qt Quick TableView (Qt Quick Controls 1.4) with “TableViewColumn’s”.
        That could be the reason, why the index.column() is always 0.
        Or do you know, how it is possible to “use” index.column()?
        Kind Regards,
        Thomas

        1. Giuseppe D'Angelo

          As far as I know, QQC1’s TableView also operates on list-like models, using multiple roles in column 0 (in the model) to populate the columns in the view. For that particular view, the model is still a list — the view maps roles onto columns using the mapping defined by the TableViewCOlumn elements.

          TableView in Qt Quick itself (introduced in 5.12 IIRC) is instead able to operate on actual table models.

Leave a Reply

Your email address will not be published. Required fields are marked *