/ models

Data Models Deconstructed - Part 2: QAbstractListModel

NOTE: This is Part 2 of the Data Models Deconstructed series, see Part 1 here.

QAbstractListModel is the tried and true way of exposing data from Qt C++ to QML. This abstract class provides an interface or contract that is adhered to by QtQuick elements such as ListView, PathView, GridView, and Repeater.

Because of this contract set forth by QAbstractListModel, Qt is able to provide a very efficient mechanism of data exposure to QML.

Efficiencies can be seen in many things including:

  • Inserting items to a data set
  • Moving items in a data set
  • Editing items in a data set
  • Removing items from a data set

These dataset manipulation methods allow us to update QML UIs or views without heavy performance impact.

If a list of top 100 music tracks needs to be displayed in QML, and one track at index #10 is changed, we don't need to rebuild the entire dataset and the list UI doesn't need to be completely re-drawn. The element at index #10 is modified in the dataset, and correspondingly the element at index #10 in the list UI is updated without modifying the list scroll position or any other visual element of the list.

datamodel-qabstractlistmodel-tracks

QAbstractListModel Basics

Since QAbstractListModel is an abstract class, it provides an interface or contract that must be implemented or adhered to by the user.

It is essentially useless in itself as it is an interface. Extend it, implement it.

int rowCount();
QVariant data();
QHash<int, QByteArray> roleNames()

There is a good amount of detail on how to do this here:
http://doc.qt.io/qt-5/qabstractlistmodel.html

QAbstractListModel Caveats

If you read the above Basics section and the Qt documentation linked above, you may notice that the roleNames requirement is quite cumbersome.

If you have a model of music tracks, each model item would be a music track.

Imagine a QML-based API looking like this:

MusicTrackModel {
    MusicTrack { }
    MusicTrack { }
    MusicTrack { }
    MusicTrack { }
}

Here, MusicTrackModel is the class name of your model, and each MusicTrack is an item within your model.

Say the MusicTrack is defined as:

QtObject {
    property string title
    property string artistName
    property int duration
}

Each property of the MusicTrack item is considered a roleName of the MusicTrackModel.

The C++ shortened implementation of the MusicTrackModel would look like this:


...
...
...

class MusicTrackModel : QAbstractListModel {
    Q_OBJECT

public:
    enum ModelRoles {
        TitleRole = Qt::UserRole + 1,
        ArtistNameRole,
        DurationRole
    };
    
    ...
    ...
    ...
    
    QHash<int, QByteArray> roleNames() const
    {
        QHash<int, QByteArray> roles;
        roles[TitleRole] = "title";
        roles[ArtistNameRole] = "artistName";
        roles[DurationRole] = "duration";
        return roles;
    }
}

As can be seen, there is tight coupling between the model and the model item. This may be useful for some cases where this model couples with another object to sort / filter the data before sending it to the QML-side of the codebase.

However; for most use-cases, the model items, i.e. MusicTrack, will be QObject-derived objects with properties that are distinguishable via the QMetaObject. If having restricted properties per model item is not a concern, you can build a generic model for all QObject-derived items. These items can be defined in Qt or in QML.

Building a generic model that holds any QObject-derived class

When designing an object that will be exposed to QML, I have found it easier to start with the API of the object, then work towards the implementation

Designing the QML-facing API

Let's start with the QML API for what a generic model that can hold any QObject-derived class:


DataObjectModel {
    id: _dataObjectModel
    
    QtObject {
        property string title: "The first track"
        property string artistName: "John Doe"
        property int duration: 230213
    }
    QtObject {
        property string title: "The second track"
        property string artistName: "John Doe"
        property int duration: 123111
    }
    QtObject {
        property string title: "The third track"
        property string artistName: "John Doe"
        property int duration: 12239991
    }
}

ListView {
    ...
    model: _dataObjectModel
}

As we can see here, we would like a model that can be fed into a QtQuick view; i.e. a ListView. This model is constructed by placing QtObject's within it.

Let's simplify the API by creating a new component called MusicTrack, e.g. MusicTrack.qml

QtObject {
    property string title
    property string artistName
    property int duration
}

Now, our simplified API use-case is:


DataObjectModel {
    id: _dataObjectModel
    
    MusicTrack {
        title: "The first track"
        artistName: "John Doe"
        duration: 230213
    }
    MusicTrack {
        title: "The second track"
        artistName: "John Doe"
        duration: 123111
    }
    MusicTrack {
        title: "The third track"
        artistName: "John Doe"
        duration: 12239991
    }
}

ListView {
    ...
    model: _dataObjectModel
}

Building the QAbstractListModel implementation

Let's call our model DataObjectModel to fit our API design.

DataObjectModel doesn't seem to have any properties in the example above, but let's consider the hidden API of a standard QtQuick-provided model, i.e. ListModel

A ListModel has:

  • count - The total number of items in the model
  • append(..) - A method to append an object to the model
  • insert(.., ..) - A method to insert an object to a particular index of the model
  • get(..) - A method to get the object at a particular index from the model

This is a standard API amongst many pre-built models in QtQuick. Let's include these in our DataObjectModel.

Let's start by creating a new C++ class in your project with QtCreator.

Go to File > New File or Project

Screen-Shot-2017-12-28-at-3.07.16-PM

Name your class DataObjectModel and derive it from QObject, for now:

Screen-Shot-2017-12-28-at-3.17.53-PM

You should now have a header file, dataobjectmodel.h that looks like this:

#ifndef DATAOBJECTMODEL_H
#define DATAOBJECTMODEL_H

#include <QObject>

class DataObjectModel : public QObject
{
    Q_OBJECT
public:
    explicit DataObjectModel(QObject *parent = nullptr);

signals:

public slots:
};

#endif // DATAOBJECTMODEL_H

I prefer using #pragma once with C++11, instead of the defines that are generated by by QtCreator templates:

dataobjectmodel.h

#pragma once

#include <QObject>

class DataObjectModel : public QObject
{
    Q_OBJECT
public:
    explicit DataObjectModel(QObject *parent = nullptr);

signals:

public slots:
};

Now, let's derive our class from QAbstractListModel:

dataobjectmodel.h

#pragma once

#include <QAbstractListModel>

class DataObjectModel : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit DataObjectModel(QObject *parent = nullptr);

signals:

public slots:
};

dataobjectmodel.cpp

#include "dataobjectmodel.h"

DataObjectModel::DataObjectModel(QObject *parent)
    : QAbstractListModel(parent)
{

}

Build it, make sure your code is building!

Now, let's setup the properties from the API we designed earlier:

Start with the count property:

Add a Q_PROPERTY under the Q_OBJECT macro:

...
Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
...

To generate the setters / getters, simply right click on the Q_PROPERTY designator and right click, Refactor -> Generate Missing Q_PROPERTY members

Screen-Shot-2017-12-28-at-3.39.32-PM

dataobjectmodel.h

#pragma once

#include <QAbstractListModel>

class DataObjectModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)

    int m_count;

public:
    explicit DataObjectModel(QObject *parent = nullptr);

    int count() const
    {
        return m_count;
    }

signals:
    void countChanged(int count);

public slots:
    void setCount(int count)
    {
        if (m_count == count)
            return;

        m_count = count;
        emit countChanged(m_count);
    }
};

Move the implementations to the cpp file by right clicking on the signature and Refactor -> Move definition to dataobjectmodel.cpp

I also prefer moving the member variable, m_count to a specific private block at the bottom of the document:

dataobjectmodel.h

#pragma once

#include <QAbstractListModel>

class DataObjectModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)

public:
    explicit DataObjectModel(QObject *parent = nullptr);
    int count() const;

signals:
    void countChanged(int count);

public slots:
    void setCount(int count);

private:
    int m_count;
};

dataobjectmodel.cpp

#include "dataobjectmodel.h"

DataObjectModel::DataObjectModel(QObject *parent)
    : QAbstractListModel(parent)
{

}

int DataObjectModel::count() const
{
    return m_count;
}

void DataObjectModel::setCount(int count)
{
    if (m_count == count)
        return;

    m_count = count;
    emit countChanged(m_count);
}

Now, initialize the count to 0 in the cpp file:

dataobjectmodel.cpp


...

DataObjectModel::DataObjectModel(QObject *parent)
    : QAbstractListModel(parent)
    , m_count(0)
{

}

...

Let's continue building out our API:

  • count - The total number of items in the model
  • append(..) - A method to append an object to the model
  • insert(.., ..) - A method to insert an object to a particular index of the model
  • get(..) - A method to get the object at a particular index from the model

The remaining three API considerations are methods. We can create methods simply by adding Q_INVOKABLE to a standard C++ method.

dataobjectmodel.h

...
public:
    Q_INVOKABLE void append(QObject* o);
    Q_INVOKABLE void insert(QObject* o, int i);
    Q_INVOKABLE QObject* get(int i);
...

Add stub implementations into dataobjectmodel.cpp.

Now, let's step back and think about what we're building.
We are building a wrapper to a set of data that we want to expose to QML. This wrapper provides the API we designed above.

If DataObjectModel is simply a wrapper, where is the real data stored?

The data is stored inside the DataObjectModel class. Let's add a new private member called m_data and use the QList data structure to store a list of object pointers.

dataobjectmodel.h

...
private:
    int m_count;
    QList<QObject*> m_data;
...

Now, let's think back to how we can complete our wrapper to provide data from the m_data list. At the start of this article, we learned about how the QAbstractListModel is essentially an interface or contract that many different elements adhere to. We need to make sure we fulfill our side of this contract by implementing the methods required by the QAbstractListModel.

Which methods do we need to implement?

Let's add these methods for rowCount(..), data(..,..), and roleNames() to our DataObjectModel class:

    int rowCount(const QModelIndex &p) const;
    QVariant data(const QModelIndex &index, int role) const;
    QHash<int, QByteArray> roleNames() const;
  • rowCount - This method simply returns the count of items in m_data as this is our data set. The argument p is unused and can be masked in a Q_UNUSED(p) block to avoid compiler warnings.
int DataObjectModel::rowCount(const QModelIndex &p) const
{
    Q_UNUSED(p)
    return m_data.size();
}
  • data - This method is how a ListView or other view element that ingests the model can access individual data objects and properties. The argument index is the index of the element in the list – 0 of course is the first element in the the list. The argument role is the enum value of the property described by the roleNames method.
  • Since we have dynamic properties in the objects that are added to this model, the concept of roles are not needed; therefore, to satisfy our contract with QAbstractListModel, we will only have one dummy role (see the roleNames bullet point below).
  • The implementation of this method is simply returning an item at index of the m_data list where we store our data. We must wrap our object in a QVariant to satisfy the method signature. Since we only have one dummy role, we will wrap the role argument with Q_UNUSED to avoid compiler warnings.
QVariant DataObjectModel::data(const QModelIndex &index, int role) const
{
    Q_UNUSED(role)
    return QVariant::fromValue(m_data[index.row()]);
}
  • roleNames - This method is a mapping of role enums to string values of their keys. Since we are allowing dynamic roles, we essentially only have one value here which refers to our object, called dataObject. This value is a dummy role but is still used on the QML side to refer to the object inside of a delegate, i.e. in a QtQuick view element, e.g. ListView
  • The implementation of this method simply returns a QHash with one role. We use a static QHash variable called pHash as this preserves the map through consequent calls to the roleNames() method.
QHash<int, QByteArray> DataObjectModel::roleNames() const
{
    static QHash<int, QByteArray> *pHash;
    if (!pHash) {
        pHash = new QHash<int, QByteArray>;
        (*pHash)[Qt::UserRole + 1] = "dataObject";
    }
    return *pHash;
}

Now we have implemented the features required by the QAbstractListModel contract. What's left?

Well, so far, we have built a wrapper to our data set held by the member variable m_data. If you have noticed, there is nothing in this variable. m_data is empty and our count is 0.

Inserting Data Objects into the DataObjectModel

Let's discuss how we can get data into our model and subsequently into our member variable m_data. We of course have the API methods insert(.., ..) and append(..) that we must implement:

  • append - Appending an object to our model simply involves us appending the object to our list variable, m_data, and emitting the necessary change signals – to adhere to our contract – so the user of our QAbstractListModel implementation knows that our data has been updated.
  • The implementation of this method starts by retrieving the current count of the data set. As we are appending an object to the end of our list, we can use this count value to instruct the QAbstractListModel interface as to where we are inserting our rows using the beingInsertRows method. We then append our object to the list m_data. Since count was a Q_PROPERTY, it is advisable to emit a changed signal as anyone who is bound to this on the QML side should receive an update when an object is appended. Finally, signal through the QAbstractListModel that we are done with the endInsertRows method.
void DataObjectModel::append(QObject *o) {
    int i = m_data.size();
    beginInsertRows(QModelIndex(), i, i);
    m_data.append(o);
    
    // Emit changed signals
    emit countChanged(count());
    
    endInsertRows();
}

modelview-begin-append-rows

  • insert - Inserting an element allows a user to insert an object to a specified index of the model.
  • The implementation is nearly identical to the implementation of the append method:
void DataObjectModel::insert(QObject *o, int i)
{
    beginInsertRows(QModelIndex(), i, i);
    m_data.insert(i, o);

    // Emit changed signals
    emit countChanged(count());

    endInsertRows();
}

It is not necessary to invoke a beginMoveRows for the rows that are displaced by this insertion. In my testing, this seems to be handled internally.

modelview-begin-insert-rows

See the Qt Documentation for more information regarding beginInsertRows

It seems we have implemented the entire API for DataObjectModel! Slow down, we have one more thing to do. Remember how we wanted to use DataObjectModel in QML?


DataObjectModel {
    id: _dataObjectModel
    
    MusicTrack {
        title: "The first track"
        artistName: "John Doe"
        duration: 230213
    }
    MusicTrack {
        title: "The second track"
        artistName: "John Doe"
        duration: 123111
    }
    MusicTrack {
        title: "The third track"
        artistName: "John Doe"
        duration: 12239991
    }
}

ListView {
    ...
    model: _dataObjectModel
}

The MusicTrack objects are children of the DataObjectModel. Sadly, being children of an object is not enough for us to be able to automatically insert them into our list m_data. We need add a default property to where the MusicTrack or any other QObject-derived class will be parented to when placed inside the DataObjectModel QML block.

DataObjectModel {
    // How do we get any element that is instantiated here to be
    // added to our model?
}

Let's start by defining the QQmlListProperty that will be holding a reference to the our MusicTrack objects.

Add a new public method called content that is a QQmlListProperty of QObject, i.e. QQmlListProperty<QObject>

dataobjectmodel.h

...
public:
    explicit DataObjectModel(QObject *parent = nullptr);
    QQmlListProperty<QObject> content();
...

This QQmlListProperty will define callbacks that will be executed whenever the QML object tree is modified.

We can see how this works in our implementation of the content() method. We have added 4 slots or callbacks to the return value for appending, count, at and clear.

We will declare and define these static slots later.

dataobjectmodel.cpp

QQmlListProperty<QObject> DataObjectModel::content()
{
    return QQmlListProperty<QObject>(this,
                                     0,
                                     &DataObjectModel::dataObjectAppend,
                                     &DataObjectModel::dataObjectCount,
                                     &DataObjectModel::dataObjectAt,
                                     &DataObjectModel::dataObjectClear);
}

QQmlListProperty will call those static slots for append, count, at, and clear whenever an element is modified in the QML object tree. We pass in this, or a reference to the DataObjectModel instance itself into the QQmlListProperty constructor so we can get a reference to it in the implementation of the static callbacks below.

See Qt Documentation for more information.

Let's define these methods:

dataobjectmodel.h

...
public slots:
    static void dataObjectAppend(QQmlListProperty<QObject> *list, QObject *e);
    static int dataObjectCount(QQmlListProperty<QObject> *list);
    static QObject* dataObjectAt(QQmlListProperty<QObject> *list, int i);
    static void dataObjectClear(QQmlListProperty<QObject> *list);
...

And declare them:

dataobjectmodel.cpp

...
void DataObjectModel::dataObjectAppend(QQmlListProperty<QObject> *list, QObject *o)
{
    DataObjectModel *dom = qobject_cast<DataObjectModel*>(list->object);
    if (dom && o) {
        dom->append(o);
    }
}

int DataObjectModel::dataObjectCount(QQmlListProperty<QObject> *list)
{
    DataObjectModel *dom = qobject_cast<DataObjectModel*>(list->object);
    if (dom) {
        return dom->m_data.count();
    }
    return 0;
}

QObject *DataObjectModel::dataObjectAt(QQmlListProperty<QObject> *list, int i)
{
    DataObjectModel *dom = qobject_cast<DataObjectModel*>(list->object);
    if (dom) {
        return dom->get(i);
    }
    return 0;
}

void DataObjectModel::dataObjectClear(QQmlListProperty<QObject> *list)
{
    DataObjectModel *dom = qobject_cast<DataObjectModel*>(list->object);
    if (dom) {
        dom->m_data.clear();
    }
}
...

For each of these callbacks, we simply grab a reference to our DataObjectModel instance and execute commands to manipulate our data set represented by the member variable m_data.

Each MusicTrack object would be received as an argument o into the dataObjectAppend function.

Finally, to ensure the MOC associates the content method as the DataObjectModel's default property, we must add two lines, one for the Q_PROPERTY declaration and another with the Q_CLASSINFO declaration.

...
// Include the QQmlListProperty class
#include <QQmlListProperty>

class DataObjectModel : public QAbstractListModel {
    Q_OBJECT
    Q_DISABLE_COPY(DataObjectModel)
    Q_PROPERTY(int count READ count NOTIFY countChanged)

    // Add these two lines
    Q_PROPERTY(QQmlListProperty<QObject> content READ content)
    Q_CLASSINFO("DefaultProperty", "content")
...

Voila! Your class is now ready to use in QML. Simply register it via qmlRegisterType and you can begin using it!

Example Usage


import QtQuick 2.7

Window {
    id: root
    
    DataObjectModel {
        id: _dataObjectModel

        MusicTrack {
            title: "The first track"
            artistName: "John Doe"
            duration: 230213
        }
        MusicTrack {
            title: "The second track"
            artistName: "John Doe"
            duration: 123111
        }
        MusicTrack {
            title: "The third track"
            artistName: "John Doe"
            duration: 12239991
        }
    }

    ListView {
        anchors.fill: parent
        model: _dataObjectModel
        delegate: Item {
            width: ListView.view.width
            height: 40
            
            Text {
                anchors.centerIn: parent
                font.bold: true
                // NOTE: This is where the roleName comes into play
                // There is a magic object called "dataObject" that references the
                // MusicTrack object for this particular index
                text: dataObject.title + " by " + dataObject.artistName
            }
        }
    }
}

And that's it!

Screen-Shot-2017-12-28-at-5.30.18-PM

Sample code is available in the qml.guide GitHub repository.

Niraj Desai

Niraj Desai

UI enthusiast, Qt / QML loyalist, passion for automotive UX – previously at Mercedes-Benz R&D, currently at NIO US.

Read More