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.
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
Name your class DataObjectModel
and derive it from QObject, for now:
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
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 argumentp
is unused and can be masked in aQ_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 argumentindex
is the index of the element in the list – 0 of course is the first element in the the list. The argumentrole
is the enum value of the property described by theroleNames
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 them_data
list where we store our data. We must wrap our object in aQVariant
to satisfy the method signature. Since we only have one dummy role, we will wrap therole
argument withQ_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 adelegate
, 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 staticQHash
variable calledpHash
as this preserves the map through consequent calls to theroleNames()
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 ourQAbstractListModel
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 thebeingInsertRows
method. We then append our object to the listm_data
. Sincecount
was aQ_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 theQAbstractListModel
that we are done with theendInsertRows
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();
}
- 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.
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!
Sample code is available in the qml.guide GitHub repository.