Qt Model / View

Understanding the basic principle

The Shock

The first time I’ve read the Qt Model / View documentation, I was shocked. Flabbergasted. Blown away. I started with a vague idea of the purpose of the Model / View, and finished convinced that I understood even less. My first implementation of the pattern was an utter failure: it looked like a Frankenstein implementation of a boilerplate code written by a crazy monkey. This was not right, the Qt documentation is known to be of quality — detailed, explanatory and coherent.

After multiple attempts and many hours reading and re-reading the doc, I started to grasp the concept. The deep understanding came when I had to write about this topic. Heck, I’ve written whole chapters on the Model / View!

I entertain myself imagining how this marathon-length documentation came to be:

A Qt framework developer glances over the Model / View documentation and utters to himself “This is not right. It makes no sense at all”. He then pounds on his keyboard in a sweated rush of the documentation-muse inspiration. He looks at his masterpiece and proudly says “It’s now much clearer! Let’s add my stone to the cathedral and update the official doc”.

After several iterations of this episode, the current doc now takes longer to read than The Lord Of the Rings. And boy, it’s a long story! Don’t get me wrong, the documentation is exhaustive in the deepest sense of exhaustiveness. But, it’s also exhausting when you discover the Model / View pattern. The underlying principle is drown in a plethora of details which are useless at the beginning.

This article aims for exactly that, understanding the Model / View pattern with a simple example. I won’t cover all the bells and whistles that you can implement with Model / View.

TL;DR — Show me the code!

You just want the source code to understand all by yourself? Everything is available on GitHub. Don’t worry, it’s less than 150 lines of C++.

The Goal

The goal is simple: build a simple Model / View example of a list of countries. It will be a read-only Model, no editing to keep things really simple (the edition will be covered in a separate post). We’ll display a list of countries and upon a country selection, a country details widget will be updated.

The final Model / View application

The final Model / View application

A country is composed of 3 pieces of data:

  • Name
  • Flag
  • Capital

The Principle

In my mind, the Model / View has 2 main goals:

  1. Cleanly separate the model’s data source from the display. This makes the update of the data source less prone to impact the View side.
  2. Leverage the Qt signal / slot mechanism to update multiple views from a unified data source.

Here’s a simplified diagram of the Model / View architecture:

Model / View architecture diagram

Model / View architecture diagram

  • You feed the Model with your data source. It can be a CSV file, rows from a database query or the result of an HTTP request. The Model name is misleading, it actually handles your model classes. In our example, we’ll create a CountryModel that will be the owner of a Country list. The real data is in the Country class, the CountryModel just loads it and makes it available for potential clients.
  • The data is provided to the View through the Model. The View can be a Qt type : QListView, or QTableView. It really depends on what your model can offer (rows and/or columns) and what type of display your want. To enforce the separation, the format is completely domain agnostic. Here, there’s no Country class at all, only a generic bundle. We’ll come back soon to this.
  • The user interacts with the View to produce events — select a row, delete a row, etc. Each event triggers an exchange between the View and the Model. The user wants to delete a row? Tell the Model about it and wait for the result with a slot.

The beauty of this architecture is that it fulfills our 2 goals:

  1. The data source can be completely changed from CSV values to Database queries, the View would not have any idea about it.
  2. Because the Model provides signals and slots, we can even connect multiple Views on a single Model. Each View will connect to the Model’ slots and get any update.

The Model

The code is done inside a Qt Widget project. We’ll start the basic build block — the Country.h class:

#include <QString>

struct Country {
    QString name;
    QString capital;
    QString flagIcon;

    Country(const QString& name, const QString& capital, const QString& flagIcon) :
        name(name),
        capital(capital),
        flagIcon(flagIcon) { }
};

The flagIconis the path to the PNG icon stored in a .qrc file, nothing fancy. The real deal starts with CountryModel.h, the owner of a Country list:

class CountryModel : public QAbstractListModel
{
    Q_OBJECT
public:

    enum CountryRoles {
        CapitalRole = Qt::UserRole,
    };

    CountryModel(QObject* parent = 0);
    ~CountryModel();

    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
    QVariant data(const QModelIndex& index, int role) const override;

private:
    QVector<Country*> countries;
};

Let’s break it down. CountryModelextends QAbstractListModel, which is a kind of Model that provides rows of data. The View displaying CountryModel’s data will configure itself by calling:

  • rowCount() to measure its widget size according to the number of rows.
  • data() to retrieve the actual data. It returns a QVariant, the generic Qt union type to transfer data back and forth without being restricted by a strong C++ type.

These functions come from QAbstractListModel, so we have to override them to provide the real data size and content. For our purpose, the data is simply a QVector of Country*.

Now, rowCount() and data() both take a special argument: a QModelIndex. We’ll dive in greater length about this topic in the post about the editable Model / View. For now, just consider it as an opaque object that handles data indexes from the Model’s perspective. To retrieve a Country from countries at a specific index, we’ll do an index.row(). Things get hairy when we update the data — adding, deleting lines — because this will mess up the index. That’s why Qt uses an internal index handler to keep track of what moved with QModelIndex.

Finally, the enum CountryRoles. We’ll skip it for now. It will come to light in the implementation of the function data().

Let’s switch the CountryModel.cpp, with CountryModel’s contructor & destructor:

#include <QIcon>

CountryModel::CountryModel(QObject* parent) :
    QAbstractListModel(parent),
    countries()
{
    countries
          << new Country("France", "Paris", ":/flags/fr.png")
          << new Country("Great Britain", "London", ":/flags/gb.png")
          << new Country("Japan", "Tokyo", ":/flags/jp.png")
          << new Country("United States of America", "Washington, D.C", ":/flags/us.png");
}

CountryModel::~CountryModel()
{
    qDeleteAll(countries);
}

countries is initialized in the constructor. You can replace it with whatever your data source is: database, CSV file, etc. The country’s icon is a path pointing to a .qrc resource stored in the project. The destructor just cleans up the Country*stored in countries.

The meat of the class lies in the following functions, starting with rowCount():

int CountryModel::rowCount(const QModelIndex& parent) const
{
    Q_UNUSED(parent)
    return countries.size();
}

The function takes a QModelIndex& parent, because you data source could be tree (think of a directory / files structure). Each directory would be a parent, and its size would be different from its neighbors. As our model is a flat list, we can safely ignore the parent and return countries.size().

Next, the implementation of data():

QVariant CountryModel::data(const QModelIndex& index, int role) const
{
    const Country& country = *countries.at(index.row());

    switch (role) {
    case Qt::DisplayRole:
        return country.name;

    case Qt::DecorationRole:
        return QIcon(country.flagIcon);

    case CountryRoles::CapitalRole:
        return country.capital;

    default:
        return QVariant();
        break;
    }
}

So, data() takes 2 parameters:

  • index to know the index of the requested data — the country index in our case.
  • role to indicate the type of the data it wants to retrieve.

data() is not called to retrieve a whole row (i.e. a full Country object). It’s called for each piece of data it wants to display. How does it know which piece of data it wants to display? That’s the point of the role parameter! There are predefined roles that can be requested by the View. In a QListView, the content of a displayed row will be Qt::DisplayRole.

First, we retrieve the Country for a given index.row(). Then, the switch(role) { … } block comes into play. if the View want’s to see the Qt::DisplayRole, we return the country.name. If the View wants to see the Qt::DecorationRole (the icon just at the left of the row label), we return a QIcon(contry.flagIcon). Every return value must be able to be converted to a QVariant, this lets data() to have a generic signature.

Alright, the principle seems sound. But what if we want to display custom data? Here, a Country also owns a capital, which could — and will — be used by the View. That’s where the forgotten enum CountryRoles strikes back. Let me avoid you some scrolling:

enum CountryRoles {
    CapitalRole = Qt::UserRole,
};

Here, we defined a custom role, CapitalRole that takes the Qt::UserRole value. Qt::UserRole is simply the starting value (0x0100) for user defined roles. If you have more data to provide in your Model, just continue to add custom roles in your enum.

A last important detail —data() is const. In other words, it cannot changeCountryModel, and can only callconst functions. If you want to generate thumbnails on the fly for a country’s flag, you’re in for a world of pain. By design, the data() functions expects to retrieve const-data and returns a QVariant to be copied by value. There are possible workarounds with a QIdentityProxyModel. If you’re going with QML, the Qt folks were thoughtful with QQuickImageProvider.

The View

The View is straightforward, in a usual MainWindow class, The form looks like this:

UI Form of the MainWindow

UI Form of the MainWindow

  • On the left, the most important widget is countryListView, a QListView that will display the data coming from CountryModel.
  • On the right, countryCapitalLabel and countryNameLabel will be updated with the currently selected Country.

The source is short, starting with MainWindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QItemSelection>

#include "CountryModel.h"

namespace Ui {
    class MainWindow;
}

class MainWindow : public QMainWindow
{
     Q_OBJECT
public:
     explicit MainWindow(QWidget *parent = 0);
     ~MainWindow();

private slots:
     void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);

private:
     Ui::MainWindow *ui;
     CountryModel countryModel;
};

#endif // MAINWINDOW_H
  • onSelectionChanged() will be called each time a selection changes on countryListView.
  • countryModel is the CountryModel member variable that will feed the data to countryListView.

The last piece of the puzzle hides in MainWindow.cpp, starting with the constructor:

#include "MainWindow.h"
#include "ui_MainWindow.h"

#include <QItemSelectionModel>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    countryModel()
{
    ui->setupUi(this);
    setWindowTitle("Model / View sample - countries");

    ui->countryListView->setModel(&countryModel);

    QItemSelectionModel* selectionModel = ui->countryListView->selectionModel();
    connect(selectionModel, &QItemSelectionModel::selectionChanged,
            this, &MainWindow::onSelectionChanged);
}

The whole magic happens when countryListView->setModel(&countryModel) is called. Because CountryModel extends QAbstractListModel, this call is generic from countryListView’s perspective. If we decide to update CountryModel to store the countries in a database, countryListView couldn’t care less.

The next lines are here to update our custom country name / capital labels. The selection of a QListView is also handled by a Model, this time it’s a QItemSelectionModel. The implementation of onSelectionChanged() looks like this:

void MainWindow::onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
{
    Q_UNUSED(deselected)

    QModelIndexList list = selected.indexes();
    const QModelIndex& index = list.first();

    QString countryName = index.data(Qt::DisplayRole).toString();
    ui->countryNameLabel->setText(countryName);

    QString countryCapital = index.data(CountryModel::CapitalRole).toString();
    ui->countryCapitalLabel->setText(countryCapital);

}

The parameters selected & deselected encapsulate the current state of the countryListView. From here, we retrieve the currently selected indexes (QListView can support multiple selections). We take the first index, and start working with our now-old-friend, QModelIndex.

To update countryNameLabel, we get the data from the Model, using the index.data() function. Remember CountryModel::data() with the role parameter? Here it is, in all its glory! The returned QVariant is converted to a QString and then passed to countryNameLabel. The same pattern is used for countryCapitalLabel, although we rely on our custom role — CountryModel::CapitalRole — to target the right piece of data.

As you can see, there’s still no trace of CountryModel around, even though it’s working behind the scene. It took less than 150 lines of C++ code to scaffold a generic data source that can be plugged in a standard QListView. In my humble opinion, it’s quite a treat to achieve this much with so little code.

Again, everything is available on GitHub, go and take a look.

The first time I really understood Model / View like so, I wondered. Is the documentation size inversely proportional to the simplicity of the concept it tries to explain? Is it a just a smokescreen, some kind of conspiration? Is life like the Model / View documentation, so simple and yet so hard to explain?

Going deeper

If you want to dig deeper in Qt, I co-wrote a whole book about the Qt framework, Mastering Qt 5. The Model / View is covered inside out, with the full data CRUD explanation, QML implementation to connect to a Model — it takes 3 chapters to cover the material.

The book aims to teach you how to be a well-rounded Qt developer. We took our years of experience, frustrations, discoveries and joys about Qt and poured them into these 500 page. We explore the inner working of the Qt signal / slot system, build advanced multi-threaded applications, explain how Qt can work with C++11/14 and much more.

tech  c++  qt 
comments powered by Disqus