QML provides two distinct approaches for implementing animations: the standard Qt Quick property animation system and OpenGL-based scene graph rendering. This article examines a demo that illustrates the performence differences between these methods under CPU load.
Visual Comparison
The demo displays two spinning icons (see Figure 1). The left spinner uses QML's built-in NumberAnimation, while the right one leverages a custom OpenGL-based implementation via the scene graph. When a simulated blocking operation occurs (indicated by "Blocked!" text), the left spinner visibly stutters every ~400ms, whereas the right spinner remains smooth—demonstrating thread separation in rendering.
Scene Graph Implementation (Right Spinner)
A custom Spinner class inherits from QQuickItem to integrate with QML:
#ifndef SPINNER_H
#define SPINNER_H
#include <QtQuick/QQuickItem>
class Spinner : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(bool spinning READ spinning WRITE setSpinning NOTIFY spinningChanged)
public:
Spinner();
bool spinning() const { return m_spinning; }
void setSpinning(bool spinning);
protected:
QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;
signals:
void spinningChanged();
private:
bool m_spinning = false;
};
The implementation creates a specialized scene graph node (SpinnerNode) that handles rendering:
QSGNode *Spinner::updatePaintNode(QSGNode *old, UpdatePaintNodeData *)
{
auto node = static_cast<SpinnerNode*>(old);
if (!node)
node = new SpinnerNode(window());
node->setSpinning(m_spinning);
return node;
}
The SpinnerNode class manages texture loading and rotation:
class SpinnerNode : public QObject, public QSGTransformNode
{
Q_OBJECT
public:
explicit SpinnerNode(QQuickWindow *window)
: m_window(window)
{
connect(window, &QQuickWindow::beforeRendering, this, &SpinnerNode::rotateIfNeeded);
connect(window, &QQuickWindow::frameSwapped, this, &SpinnerNode::requestUpdate);
auto texture = window->createTextureFromImage(QImage(":/spinner.png"));
auto textureNode = new QSGSimpleTextureNode();
textureNode->setTexture(texture);
textureNode->setRect(0, 0, 64, 64);
appendChildNode(textureNode);
m_texture.reset(texture);
}
private slots:
void rotateIfNeeded() {
if (!m_spinning) return;
m_rotation += 360.0 / m_window->screen()->refreshRate();
QMatrix4x4 transform;
transform.translate(32, 32);
transform.rotate(m_rotation, 0, 0, 1);
transform.translate(-32, -32);
setMatrix(transform);
}
void requestUpdate() {
if (m_spinning) m_window->update();
}
private:
qreal m_rotation = 0;
bool m_spinning = false;
QScopedPointer<QSGTexture> m_texture;
QQuickWindow *m_window;
};
Property Animation (Left Spinner)
The left spinner uses declarative QML animation:
Image {
source: "spinner.png"
anchors.centerIn: parent
anchors.horizontalCenterOffset: -80
NumberAnimation on rotation {
from: 0; to: 360
duration: 1000
loops: Animation.Infinite
}
}
Bolcking Simulation
A JavaScript timer simulates CPU-intensive work:
Timer {
id: blocker
interval: 357
repeat: true
onTriggered: {
const start = Date.now();
while (Date.now() - start < 100) {
// Busy-wait to block main thread
}
}
}
This blocking operation affects only the main-thread animation (left spinner), while the scene graph renderer (right spinner) continues smoothly since it operates on a separate render thread when using threaded render loops.