Models in C++

One of the most common ways to integrate C++ and QML is through models. A model provides data to a view such as ListViews, GridView, PathViews, and other views which take a model and create an instance of a delegate for each entry in the model. The view is smart enough to only create these instances which are visible or in the cache range. This makes it possible to have large models with tens of thousands of entries but still have a very slick user interface. The delegate acts like a template to be rendered with the model entries data. So in summary: a view renders entries from the model using a delegate as a template. The model is a data provider for views.

When you do not want to use C++ you can also define models in pure QML. You have several ways to provide a model for the view. For handling of data coming from C++ or a large amount of data, the C++ model is more suitable than this pure QML approach. But often you only need a few entries then these QML models are well suited.

  1. ListView {
  2. // using a integer as model
  3. model: 5
  4. delegate: Text { text: 'index: ' + index }
  5. }
  6. ListView {
  7. // using a JS array as model
  8. model: ['A', 'B', 'C', 'D', 'E']
  9. delegate: Text { 'Char['+ index +']: ' + modelData }
  10. }
  11. ListView {
  12. // using a dynamic QML ListModel as model
  13. model: ListModel {
  14. ListElement { char: 'A' }
  15. ListElement { char: 'B' }
  16. ListElement { char: 'C' }
  17. ListElement { char: 'D' }
  18. ListElement { char: 'E' }
  19. }
  20. delegate: Text { 'Char['+ index +']: ' + model.char }
  21. }

The QML views know how to handle these different types of models. For models coming from the C++ world, the view expects a specific protocol to be followed. This protocol is defined in the API defined in QAbstractItemModel, together with documentation describing the dynamic behavior. The API was developed for driving views in the desktop widget world and is flexible enough to act as a base for trees, or multi-column tables as well as lists. In QML, we usually use either the list variant of the API, QAbstractListModel or, for the TableView element, the table variant of the API, QAbstractTableModel. The API contains some functions that have to be implemented and some optional ones that extend the capabilities of the model. The optional parts mostly handle the dynamic use cases for changing, adding or deleting data.

A simple model

A typical QML C++ model derives from QAbstractListModel and implements at least the data and rowCount function. In the example below, we will use a series of SVG color names provided by the QColor class and display them using our model. The data is stored into a QList<QString> data container.

Our DataEntryModel is derived form QAbstractListModel and implements the mandatory functions. We can ignore the parent in rowCount as this is only used in a tree model. The QModelIndex class provides the row and column information for the cell, for which the view wants to retrieve data. The view is pulling information from the model on a row/column and role-based. The QAbstractListModel is defined in QtCore but QColor in QtGui. That is why we have the additional QtGui dependency. For QML applications it is okay to depend on QtGui but it should normally not depend on QtWidgets.

  1. #ifndef DATAENTRYMODEL_H
  2. #define DATAENTRYMODEL_H
  3. #include <QtCore>
  4. #include <QtGui>
  5. class DataEntryModel : public QAbstractListModel
  6. {
  7. Q_OBJECT
  8. public:
  9. explicit DataEntryModel(QObject *parent = 0);
  10. ~DataEntryModel();
  11. public: // QAbstractItemModel interface
  12. virtual int rowCount(const QModelIndex &parent) const;
  13. virtual QVariant data(const QModelIndex &index, int role) const;
  14. private:
  15. QList<QString> m_data;
  16. };
  17. #endif // DATAENTRYMODEL_H

On the implementation side, the most complex part is the data function. We first need to make a range check to ensure that we’ve been provided a valid index. Then we check that the display role is supported. Each item in a model can have multiple display roles defining various aspects of the data contained. The Qt::DisplayRole is the default text role a view will ask for. There is a small set of default roles defined in Qt which can be used, but normally a model will define its own roles for clarity. In the example, all calls which do not contain the display role are ignored at the moment and the default value QVariant() is returned.

  1. #include "dataentrymodel.h"
  2. DataEntryModel::DataEntryModel(QObject *parent)
  3. : QAbstractListModel(parent)
  4. {
  5. // initialize our data (QList<QString>) with a list of color names
  6. m_data = QColor::colorNames();
  7. }
  8. DataEntryModel::~DataEntryModel()
  9. {
  10. }
  11. int DataEntryModel::rowCount(const QModelIndex &parent) const
  12. {
  13. Q_UNUSED(parent);
  14. // return our data count
  15. return m_data.count();
  16. }
  17. QVariant DataEntryModel::data(const QModelIndex &index, int role) const
  18. {
  19. // the index returns the requested row and column information.
  20. // we ignore the column and only use the row information
  21. int row = index.row();
  22. // boundary check for the row
  23. if(row < 0 || row >= m_data.count()) {
  24. return QVariant();
  25. }
  26. // A model can return data for different roles.
  27. // The default role is the display role.
  28. // it can be accesses in QML with "model.display"
  29. switch(role) {
  30. case Qt::DisplayRole:
  31. // Return the color name for the particular row
  32. // Qt automatically converts it to the QVariant type
  33. return m_data.value(row);
  34. }
  35. // The view asked for other data, just return an empty QVariant
  36. return QVariant();
  37. }

The next step would be to register the model with QML using the qmlRegisterType call. This is done inside the main.cpp before the QML file was loaded.

  1. #include <QtGui>
  2. #include <QtQml>
  3. #include "dataentrymodel.h"
  4. int main(int argc, char *argv[])
  5. {
  6. QGuiApplication app(argc, argv);
  7. // register the type DataEntryModel
  8. // under the url "org.example" in version 1.0
  9. // under the name "DataEntryModel"
  10. qmlRegisterType<DataEntryModel>("org.example", 1, 0, "DataEntryModel");
  11. QQmlApplicationEngine engine;
  12. engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
  13. return app.exec();
  14. }

Now you can access the DataEntryModel using the QML import statement import org.example 1.0 and use it just like other QML item DataEntryModel {}.

We use this in this example to display a simple list of color entries.

  1. import org.example 1.0
  2. ListView {
  3. id: view
  4. anchors.fill: parent
  5. model: DataEntryModel {}
  6. delegate: ListDelegate {
  7. // use the defined model role "display"
  8. text: model.display
  9. }
  10. highlight: ListHighlight { }
  11. }

The ListDelegate is a custom type to display some text. The ListHighlight is just a rectangle. The code was extracted to keep the example compact.

The view can now display a list of strings using the C++ model and the display property of the model. It is still very simple, but already usable in QML. Normally the data is provided from outside the model and the model would act as an interface to the view.

TIP

To expose a table of data instead of a list, the QAbstractTableModel is used. The only difference compared to implementing a QAbstractListModel is that you must also provide the columnCount method.

More Complex Data

In reality, the model data is often much more complex. So there is a need to define custom roles so that the view can query other data via properties. For example, the model could provide not only the color as a hex string but maybe also the hue, saturation, and brightness from the HSV color model as “model.hue”, “model.saturation” and “model.brightness” in QML.

  1. #ifndef ROLEENTRYMODEL_H
  2. #define ROLEENTRYMODEL_H
  3. #include <QtCore>
  4. #include <QtGui>
  5. class RoleEntryModel : public QAbstractListModel
  6. {
  7. Q_OBJECT
  8. public:
  9. // Define the role names to be used
  10. enum RoleNames {
  11. NameRole = Qt::UserRole,
  12. HueRole = Qt::UserRole+2,
  13. SaturationRole = Qt::UserRole+3,
  14. BrightnessRole = Qt::UserRole+4
  15. };
  16. explicit RoleEntryModel(QObject *parent = 0);
  17. ~RoleEntryModel();
  18. // QAbstractItemModel interface
  19. public:
  20. virtual int rowCount(const QModelIndex &parent) const override;
  21. virtual QVariant data(const QModelIndex &index, int role) const override;
  22. protected:
  23. // return the roles mapping to be used by QML
  24. virtual QHash<int, QByteArray> roleNames() const override;
  25. private:
  26. QList<QColor> m_data;
  27. QHash<int, QByteArray> m_roleNames;
  28. };
  29. #endif // ROLEENTRYMODEL_H

In the header, we added the role mapping to be used for QML. When QML tries now to access a property from the model (e.g. “model.name”) the listview will lookup the mapping for “name” and ask the model for data using the NameRole. User-defined roles should start with Qt::UserRole and need to be unique for each model.

  1. #include "roleentrymodel.h"
  2. RoleEntryModel::RoleEntryModel(QObject *parent)
  3. : QAbstractListModel(parent)
  4. {
  5. // Set names to the role name hash container (QHash<int, QByteArray>)
  6. // model.name, model.hue, model.saturation, model.brightness
  7. m_roleNames[NameRole] = "name";
  8. m_roleNames[HueRole] = "hue";
  9. m_roleNames[SaturationRole] = "saturation";
  10. m_roleNames[BrightnessRole] = "brightness";
  11. // Append the color names as QColor to the data list (QList<QColor>)
  12. for(const QString& name : QColor::colorNames()) {
  13. m_data.append(QColor(name));
  14. }
  15. }
  16. RoleEntryModel::~RoleEntryModel()
  17. {
  18. }
  19. int RoleEntryModel::rowCount(const QModelIndex &parent) const
  20. {
  21. Q_UNUSED(parent);
  22. return m_data.count();
  23. }
  24. QVariant RoleEntryModel::data(const QModelIndex &index, int role) const
  25. {
  26. int row = index.row();
  27. if(row < 0 || row >= m_data.count()) {
  28. return QVariant();
  29. }
  30. const QColor& color = m_data.at(row);
  31. qDebug() << row << role << color;
  32. switch(role) {
  33. case NameRole:
  34. // return the color name as hex string (model.name)
  35. return color.name();
  36. case HueRole:
  37. // return the hue of the color (model.hue)
  38. return color.hueF();
  39. case SaturationRole:
  40. // return the saturation of the color (model.saturation)
  41. return color.saturationF();
  42. case BrightnessRole:
  43. // return the brightness of the color (model.brightness)
  44. return color.lightnessF();
  45. }
  46. return QVariant();
  47. }
  48. QHash<int, QByteArray> RoleEntryModel::roleNames() const
  49. {
  50. return m_roleNames;
  51. }

The implementation now has changed only in two places. First in the initialization. We now initialize the data list with QColor data types. Additionally, we define our role name map to be accessible for QML. This map is returned later in the ::roleNames function.

The second change is in the ::data function. We extend the switch to cover the other roles (e.g hue, saturation, brightness). There is no way to return an SVG name from a color, as a color can take any color and SVG names are limited. So we skip this. Storing the names would require to create a structure struct { QColor, QString } to be able to identify the named color.

After registering the type we can use the model and its entries in our user interface.

  1. ListView {
  2. id: view
  3. anchors.fill: parent
  4. model: RoleEntryModel {}
  5. focus: true
  6. delegate: ListDelegate {
  7. text: 'hsv(' +
  8. Number(model.hue).toFixed(2) + ',' +
  9. Number(model.saturation).toFixed() + ',' +
  10. Number(model.brightness).toFixed() + ')'
  11. color: model.name
  12. }
  13. highlight: ListHighlight { }
  14. }

We convert the returned type to a JS number type to be able to format the number using fixed-point notation. The code would also work without the Number call (e.g. plain model.saturation.toFixed(2)). Which format to choose, depends how much you trust the incoming data.

Dynamic Data

Dynamic data covers the aspects of inserting, removing and clearing the data from the model. The QAbstractListModel expect a certain behavior when entries are removed or inserted. The behavior is expressed in signals which need to be called before and after the manipulation. For example to insert a row into a model you need first to emit the signal beginInsertRows, then manipulate the data and then finally emit endInsertRows.

We will add the following functions to our headers. These functions are declared using Q_INVOKABLE to be able to call them from QML. Another way would be to declare them as public slots.

  1. // inserts a color at the index (0 at begining, count-1 at end)
  2. Q_INVOKABLE void insert(int index, const QString& colorValue);
  3. // uses insert to insert a color at the end
  4. Q_INVOKABLE void append(const QString& colorValue);
  5. // removes a color from the index
  6. Q_INVOKABLE void remove(int index);
  7. // clear the whole model (e.g. reset)
  8. Q_INVOKABLE void clear();

Additionally, we define a count property to get the size of the model and a get method to get a color at the given index. This is useful when you would like to iterate over the model content from QML.

  1. // gives the size of the model
  2. Q_PROPERTY(int count READ count NOTIFY countChanged)
  3. // gets a color at the index
  4. Q_INVOKABLE QColor get(int index);

The implementation for insert checks first the boundaries and if the given value is valid. Only then do we begin inserting the data.

  1. void DynamicEntryModel::insert(int index, const QString &colorValue)
  2. {
  3. if(index < 0 || index > m_data.count()) {
  4. return;
  5. }
  6. QColor color(colorValue);
  7. if(!color.isValid()) {
  8. return;
  9. }
  10. // view protocol (begin => manipulate => end]
  11. emit beginInsertRows(QModelIndex(), index, index);
  12. m_data.insert(index, color);
  13. emit endInsertRows();
  14. // update our count property
  15. emit countChanged(m_data.count());
  16. }

Append is very simple. We reuse the insert function with the size of the model.

  1. void DynamicEntryModel::append(const QString &colorValue)
  2. {
  3. insert(count(), colorValue);
  4. }

Remove is similar to insert but it calls according to the remove operation protocol.

  1. void DynamicEntryModel::remove(int index)
  2. {
  3. if(index < 0 || index >= m_data.count()) {
  4. return;
  5. }
  6. emit beginRemoveRows(QModelIndex(), index, index);
  7. m_data.removeAt(index);
  8. emit endRemoveRows();
  9. // do not forget to update our count property
  10. emit countChanged(m_data.count());
  11. }

The helper function count is trivial. It just returns the data count. The get function is also quite simple.

  1. QColor DynamicEntryModel::get(int index)
  2. {
  3. if(index < 0 || index >= m_data.count()) {
  4. return QColor();
  5. }
  6. return m_data.at(index);
  7. }

You need to be careful that you only return a value which QML understands. If it is not one of the basic QML types or types known to QML you need to register the type first with qmlRegisterType or qmlRegisterUncreatableType. You use qmlRegisterUncreatableType if the user shall not be able to instantiate its own object in QML.

Now you can use the model in QML and insert, append, remove entries from the model. Here is a small example which allows the user to enter a color name or color hex value and the color is then appended onto the model and shown in the list view. The red circle on the delegate allows the user to remove this entry from the model. After the entry is to remove the list view is notified by the model and updates its content.

image

And here is the QML code. You find the full source code also in the assets for this chapter. The example uses the QtQuick.Controls and QtQuick.Layout module to make the code more compact. These controls module provides a set of desktop related UI elements in Qt Quick and the layouts module provides some very useful layout managers.

  1. import QtQuick 2.5
  2. import QtQuick.Window 2.2
  3. import QtQuick.Controls 1.5
  4. import QtQuick.Layouts 1.2
  5. // our module
  6. import org.example 1.0
  7. Window {
  8. visible: true
  9. width: 480
  10. height: 480
  11. Background { // a dark background
  12. id: background
  13. }
  14. // our dyanmic model
  15. DynamicEntryModel {
  16. id: dynamic
  17. onCountChanged: {
  18. // we print out count and the last entry when count is changing
  19. print('new count: ' + count);
  20. print('last entry: ' + get(count-1));
  21. }
  22. }
  23. ColumnLayout {
  24. anchors.fill: parent
  25. anchors.margins: 8
  26. ScrollView {
  27. Layout.fillHeight: true
  28. Layout.fillWidth: true
  29. ListView {
  30. id: view
  31. // set our dynamic model to the views model property
  32. model: dynamic
  33. delegate: ListDelegate {
  34. width: ListView.view.width
  35. // construct a string based on the models proeprties
  36. text: 'hsv(' +
  37. Number(model.hue).toFixed(2) + ',' +
  38. Number(model.saturation).toFixed() + ',' +
  39. Number(model.brightness).toFixed() + ')'
  40. // sets the font color of our custom delegates
  41. color: model.name
  42. onClicked: {
  43. // make this delegate the current item
  44. view.currentIndex = index
  45. view.focus = true
  46. }
  47. onRemove: {
  48. // remove the current entry from the model
  49. dynamic.remove(index)
  50. }
  51. }
  52. highlight: ListHighlight { }
  53. // some fun with transitions :-)
  54. add: Transition {
  55. // applied when entry is added
  56. NumberAnimation {
  57. properties: "x"; from: -view.width;
  58. duration: 250; easing.type: Easing.InCirc
  59. }
  60. NumberAnimation { properties: "y"; from: view.height;
  61. duration: 250; easing.type: Easing.InCirc
  62. }
  63. }
  64. remove: Transition {
  65. // applied when entry is removed
  66. NumberAnimation {
  67. properties: "x"; to: view.width;
  68. duration: 250; easing.type: Easing.InBounce
  69. }
  70. }
  71. displaced: Transition {
  72. // applied when entry is moved
  73. // (e.g because another element was removed)
  74. SequentialAnimation {
  75. // wait until remove has finished
  76. PauseAnimation { duration: 250 }
  77. NumberAnimation { properties: "y"; duration: 75
  78. }
  79. }
  80. }
  81. }
  82. }
  83. TextEntry {
  84. id: textEntry
  85. onAppend: {
  86. // called when the user presses return on the text field
  87. // or clicks the add button
  88. dynamic.append(color)
  89. }
  90. onUp: {
  91. // called when the user presses up while the text field is focused
  92. view.decrementCurrentIndex()
  93. }
  94. onDown: {
  95. // same for down
  96. view.incrementCurrentIndex()
  97. }
  98. }
  99. }
  100. }

Model view programming is one of the more complex development tasks in Qt. It is one of the very few classes where you have to implement an interface as a normal application developer. All other classes you just use normally. The sketching of models should always start on the QML side. You should envision how your users would use your model inside QML. For this, it is often a good idea to create a prototype first using the ListModel to see how this best works in QML. This is also true when it comes to defining QML APIs. Making data available from C++ to QML is not only a technology boundary it is also a programming paradigm change from imperative to declarative style programming. So be prepared for some setbacks and aha moments:-).