Exploring Model-View Design With Qt Quick
The speed on your car’s dashboard, the temperature on your cabin’s thermostat, the heart rate on your wearable device. These values originate from an external source, populate a model, and then make their digital way to a display. The task of the GUI developer is to write code that fetches this data from an external source, organizes it, and then presents it beautifully.
Writing such a model, and providing it to a view, relies on the model-view design pattern. With Qt Quick, one way to achieve this is to wrap a third-party library with a QObject and map every value on the display to a Qt property. This works well when the number of values remains constant. However, if the size of the data changes, it becomes unmanageable in a hurry.
In this blog post, part 1 of 2, we review the basic way to write a model and present Qt’s model-view-delegate (MVD) framework as a solution to the problem of displaying dynamic data.
Model-View Design Pattern
The model-view design pattern isn’t new; in fact, it’s been around since the '70s in the form of model-view-controller (MVC). This design pattern simplifies GUI development by dividing disparate data interactions into three separate parts, as shown below in Figure 1. By combining the controller with the view, a simpler architecture, the model-view (MV) is born.
The model-view approach is important for all GUI developers to understand and use because it provides a basic level of separation between frontend user interface (UI) and backend business logic. The model consists of the business logic code, written to organize all data-only logic such as socket connections, database queries, file I/O, and performing computations. The view presents this model data in a visually appealing way. Model-view enforces a loose coupling between backend logic and frontend UI, providing numerous key advantages like code reuse, testability and parallel design. Therefore, user interfaces, typically written in QML, can be written almost independently from the C++ business logic.
Figure 1: Model-view-controller data interactions [1]
Simple Model Design with Qt
In Qt Quick, one way to write a model is to implement a class that inherits from QObject, and for every entity displayed in the view, provide a property using the Q_PROPERTY macro. At minimum, a Qt property requires a READ function, used to provide the actual value of the view, and a NOTIFY signal, emitted to alert the view that it needs to update. If the UI allows modification of this value, a Qt property can specify a WRITE function for updating the value at its source.
As an example, we can create a QObject-inherited class with two properties: speed and temperature. We then associate a READ function and NOTIFY signal with each property.
class SimpleModel : public QObject
{
Q_OBJECT
Q_PROPERTY(int speed READ speed NOTIFY speedChanged)
Q_PROPERTY(int temperature READ temperature NOTIFY temperatureChanged)
public:
explicit SimpleModel(QObject *parent = nullptr);
int speed() const;
int temperature() const;
signals:
void speedChanged(int speed);
void temperatureChanged(int temperature);
};
A read function is responsible for fetching the data and relaying it to the UI.
int simpleModel::speed() const
{
int speed = grabSpeedFromThirdPartyLib(); //Grabs the speed from an external source
return speed;
}
The NOTIFY signal is emitted when the associated property changes.
emit speedChanged();
This class is exported to the UI after the QML engine is instantiated but before the main QML file is loaded.
#include
#include
#include
#include "simplemodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
SimpleModel aSimpleModel;
engine.rootContext()->setContextProperty("aSimplemodel",&aSimpleModel)
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
Then, the C++ model and properties can be retrieved in QML. The emission of the NOTIFY signals will keep the UI up to date through Qt Quick’s property binding feature.
Window {
...
Text{
...
text: aSimplemodel.speed
}
Text{
...
text: aSimplemodel.temperature
}
}
Flexible Solutions
The necessity for a more flexible solution becomes clear when a GUI designer attempts to rely on properties alone. Let’s say you have a QObject on your backend that encapsulates some business logic with properties bound to your visual QML components. What happens when your backend data structure accumulates one more attribute? That’s right: you add yet another property to your QObject and then access this property in QML. This process can get tedious if the backend data continually changes, the collection grows exponentially, or complex control over backend data access is needed.
Qt comes to the rescue with the MVD framework by first providing an abstract base class QAbstractItemModel, which, when subclassed, accomplishes the same function as properties but provides a more fine-tuned control over WHEN certain data-manipulations apply. A property may have a READ method, a WRITE method, and a NOTIFY signal specified, and these methods map one-to-one with QAbstractItemModel’s data(), setData(), and dataChanged() signal. The QAbstractItemModel further extends control over the data collection by providing signals that surround the insertion and/or deletion of model data.
Qt Model-View-Delegate Framework
The Qt MVD framework is available for both Widgets and Qt Quick. The Widget MVD framework is similar to the Qt Quick MVD framework, except that Widgets provide Widget View C++ classes (e.g QListView, QTableView, QTreeView), whereas Qt Quick provides QML view types (e.g ListView, GridView, PathView). Our discussion and corresponding example focuses on the Qt Quick MVD framework. In Qt’s MVD framework, the model data, sourced from the backend business logic, renders a view as directed by a delegate. Figure 2 below shows how data interactions are modularized in the Qt-centric MVD.
Figure 2: Model-view-delegate interactions in Qt [2]
The model encapsulates the application’s business logic, and it enforces the type and structure of the data that is served to the view. For example, a model bound to a ListView QML instance can be an integer value, JavaScript array, ListModel QML instance, or a custom C++ model that subclasses QAbstractItemModel. Aside from prototypes and mockups, the most useful model is the QAbstractItemModel C++ class, as it provides more flexibility and control as an application grows in size and complexity.
The view is the visual container that displays the model data. There are several QML types used to host a view: ListView, GridView, and PathView. Each view offers its own variation of displaying a collection of model-based data with different arrangements. Having the view be a separate entity from the model provides the benefit of model reuse in different visual representations. The delegate visually and logically encapsulates each instance of model data and renders it as part of the view. It is responsible for rendering the model data associated with one index and role in the view. This index is abstracted in Qt through the QModelIndex class, and the role is a C++ enum that is specified in the model class.
Summary
The model-view design pattern can be easily incorporated into any Qt project, either through simple properties or Qt’s MVD framework. Separating backend business logic from frontend user interface is an important aspect of GUI design, one that the MVD framework handles effortlessly. In part 2, we'll illustrate Qt Quick’s model-view-delegate with an mp3 downloading manager project.
References
- [1] https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
- [2] https://doc.qt.io/qt-5/model-view-programming.html
- https://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.html
- https://www.embedded-computing.com/guest-blogs/layered-architecture-delivers-more-reliable-automotive-applications-faste
- https://www.codeguru.com/cpp/cpp/implementing-an-mvc-model-with-the-qt-c-framework.html