Live Reloading or Hot Reloading with QML
Live reloading or hot reloading is all the rage these days. One of the best examples of hot reloading is React Native.
Check out a video of it here:
Live reloading is extremely useful to quickly develop and iterate over the user interface aspect of an application.
The majority of Qt / QML developers I have met (myself included, until recently) underestimate the capability of hot reloading QML applications. We are so accustomed to building and running an application, even for the slighest of changes (width, height, color, and other static data).
There are many solutions to the hot reloading problem in Qt + QML. They all have their own advantages and disadvantages. I prefer to use a combination of all the below to accomplish a variety of UI tasks for a project:
Terrarium
Terrarium is a live reloader documented here.
Advantages:
- Quick prototyping with a built-in editor.
- Great for QML interviews.
Disadvantages:
- Not useful for projects as it can only read from the
TextEdit
element that is built-in to the application.
Qml LiveReload
Qml LiveReload is available here.
After building, the target is placed at /usr/local/bin
(for some reason..)
To use,
/usr/local/bin/qmllive /path/to/file.qml
Advantages:
- Easy to use, simply build and execute a QML file with the binary specified above.
- Hard to use with large projects, does not preserve states
Disadvantages:
- Out of build target.. builds to
/usr/local/bin/qmllive
- Reloads the entire application
QmlLive
I lightly covered QmlLive here.
Advantages:
- Application preserves workspace, you can easily open and close the app and restore your workspace
- Allows deployment of a runtime to an embedded device to see a UI reload live on a different device.
Disadvantages:
- Reloads the entire application
- UI states are not preserved upon reload
Custom Loader + QQmlEngine Solution
This is my preferred solution. I believe it is the most flexible one as it provides reload capability to individual pieces of a project's user interface; unlike some of the aforementioned solutions which reload the entire user interface.
Before diving into the code that powers this method of live reloading, let's investigate how the QmlEngine
works with respect to the loading of QML elements.
In QML, a Component
is a template that defines an object. A Component
can be used to build objects that have a well-defined structure and interface.
Component {
id: _componentIconLabel
Row {
property var icon
property var text
Image {
width: 50; height: 50
source: parent.icon
}
Text {
text: parent.text
}
}
}
Button {
onClicked: {
var iconLabelObject = _componentIconLabel.createObject(root);
iconLabelObject.text = "Hello World";
// NOTE: These properties can be set before the
// element is created by using the createObject
// method's second argument
}
}
In the example above, we already created the Component
by declaring it in QML. You can also create a Component
by loading a file.
Once a Component
is created, it is cached in the QmlEngine
.
How does this cache work? Let's dig a little deeper into the sources.
-
Component
uses theQmlEngine
'sQmlTypeLoader
to get the type of what is being loaded into aComponent
. More information can be read in the Qt sources here. -
QmlTypeLoader
provides agetType
method that checks a cache (QmlTypeCache
), if there is a hit it returns cached data, else, it loads in new data.
To allow for hot or live reloading, it is crucial for us to ignore the cache and reload from disk. The criteria for checking cache is not particularly sophisticated as every type is registered by URL / type name.
Since we do not want modify the criteria for how Component
caching works in Qt sources, we can work around this cache by simply clearing it when a reload is required.
QmlEngine
provides two methods for manipulating the QML type component cache.
QQmlEngine::trimComponentCache()
- Trims component cache entries for components that are no longer used. This is a very difficult criteria to satisfy as components are generally used widely throughout a QML project.
See Qt sources here
QQmlEngine::clearComponentCache()
- Clears the entire component cache. If an element is still used, it is at risk of causing warnings throughout the UI as when the same type is loaded again, it will be loaded into a new cache entry. This new cache entry and the old active element will no longer match.
This is a small inconvenience for the benefit of hot reloading. By clearing the entire component cache, we can simply reload a QML file to view updated changes.
See Qt sources here.
Let's get down to business.
There are a few ways you can implement this:
- Option A: Build a custom
Component
loader in Qt C++, referenceQmlEngine
directly in C++ - Option B: Extend the QML
Loader
and expose a reference to theQmlEngine
to QML
I prefer Option B, so let's do it:
First, let's create an example project with a Window
, a Loader
, and a Component
.
main.qml
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Loader {
id: _loader
function reload() {
source = "";
source = "Circle.qml";
}
anchors.centerIn: parent
source: "Circle.qml"
}
MouseArea {
anchors.fill: parent
onClicked: {
_loader.reload();
}
}
}
Add a MouseArea
and a method in the loader called reload. We will use these to prove that the QmlEngine
and its associates are caching types.
In this example, we are creating a Loader
and load in file called Circle.qml:
Circle.qml
import QtQuick 2.9
Rectangle {
width: 300
height: 300
radius: width / 2
color: "blue"
}
Run the UI and you will see this:
Keep the UI open. Now, modify Circle.qml to make the Rectangle
color red:
Circle.qml
import QtQuick 2.9
Rectangle {
width: 300
height: 300
radius: width / 2
color: "red"
}
Now that the Circle is red, click on the UI to reload the Loader
.
Notice how the Circle remains blue? This Loader
is being reloaded yet the Circle is still blue. This is proof that the Component
created from Circle.qml is cached. It's also apparent that the cache is not keyed by the contents of Circle.qml.
Let's keep working until we can figure out how to make the Circle change colors when we click the UI.
Above, we learned about the methods available in the QmlEngine
to trim and clear the cache in the QmlEngine
. Let's use some of these methods.
To use a method from the QmlEngine
in QML land, we must expose it. Let's do it via context properties.
Here is our auto-generated (by project template) main.cpp:
original main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
#if defined(Q_OS_WIN)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
Let's use context properties to expose the engine to QML. Context properties are simply magic global variables that are available in all of QML without the need of importing anything.
modified main.qml
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
int main(int argc, char *argv[])
{
#if defined(Q_OS_WIN)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("$QmlEngine", &engine);
engine.load(QUrl(qgetenv("MAIN_QML")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
In this code, we are exposing a global variable called $QmlEngine
as a context property of the QmlEngine
's root context. This allows the $QmlEngine
variable to be used from any QML file loaded inside this this particular engine.
Also note how we replaced the qrc://
URL with a qgetenv
. We do not want Qt to compile our resources into a binary QRC file as this would require us to rebuild the project when changing a QML file.
I have removed the use of QRC in almost every large-scale project I've worked on. Compiling QML files into resource files is very detrimental to developer productivity. Compiling QML files into resources can easily be enabled for release or production builds and avoided in developer builds.
We should run our application with the environment variable specified, e.g:
MAIN_QML=../HotReloading/main.qml ./bin/HotReloading
Now that we have a reference to the QmlEngine
, let's invoke the QQmlEngine::clearComponentCache
when we are reloading the Loader
element in main.qml:
main.qml
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Loader {
id: _loader
function reload() {
source = "";
$QmlEngine.clearComponentCache();
source = "Circle.qml";
}
anchors.centerIn: parent
source: "Circle.qml"
}
MouseArea {
anchors.fill: parent
onClicked: {
_loader.reload();
}
}
}
Let's try this again. Build your code and run the UI again.
Modify the color of the Rectangle
in Circle.qml.
Click the circle. Did it change colors?
Nope! Now we got a warning:
qrc:/main.qml:15: TypeError: Property 'clearComponentCache' of object QQmlApplicationEngine(0x7fff58907a98) is not a function
What gives? It seems that QQmlEngine::clearComponentCache
is not allowed to be invoked by QML. It is a public method, but it does not have the Q_INVOKABLE
flag.
Let's create a utility class that exposes an aliased function that calls QQmlEngine::clearComponentCache
. These kinds of hoops are common when attempting to expose Qt functionality to QML.
We have two options here, we can either create a brand new class that extends QObject
or we can create a brand new class that extends QQmlApplicationEngine
. The latter is simpler as we don't need to pass in the QQmlEngine
object.
Create a new file called EnhancedQmlApplicationEngine
:
enhancedqmlapplicationengine.h
#pragma once
#include <QQmlApplicationEngine>
class EnhancedQmlApplicationEngine : public QQmlApplicationEngine
{
Q_OBJECT
public:
explicit EnhancedQmlApplicationEngine(QObject *parent = nullptr);
Q_INVOKABLE void clearCache();
};
enhancedqmlapplicationengine.cpp
#include "enhancedqmlapplicationengine.h"
EnhancedQmlApplicationEngine::EnhancedQmlApplicationEngine(QObject *parent)
: QQmlApplicationEngine(parent)
{
}
void EnhancedQmlApplicationEngine::clearCache()
{
this->clearComponentCache();
}
Note how we had to name the method clearCache instead of clearComponentCache since the latter was already taken by the super class, QQmlEngine
by proxy of QQmlApplicationEngine
.
Use this new engine in main.cpp:
main.cpp
#include <QGuiApplication>
#include <QQmlContext>
#include "enhancedqmlapplicationengine.h"
int main(int argc, char *argv[])
{
#if defined(Q_OS_WIN)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
EnhancedQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("$QmlEngine", &engine);
engine.load(QUrl(qgetenv("MAIN_QML")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
Update your reload method in main.qml to call our new clearCache method:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Loader {
id: _loader
function reload() {
source = "";
$QmlEngine.clearCache();
source = "Circle.qml";
}
anchors.centerIn: parent
source: "Circle.qml"
}
MouseArea {
anchors.fill: parent
onClicked: {
_loader.reload();
}
}
}
Voila! Build your code again.
Run the UI.
Now, you can scatter these Loader
elements with a custom reload
method throughout your UI. Whenever you need to reload, you can. This allows for rapid UI development, especially when building out individual widgets or scenes.
This Hot Reloading solution can be extended:
- Use a
QFileSystemWatcher
class to track the loaded file for changes. When a change is signaled, reload theLoader
. - Use a
Timer
to reload constantly while developing.
The source code is available on the qml.guide GitHub repository.
Subscribe to QML Guide
Get the latest posts delivered right to your inbox