QML or QWidget? A Decision Guide for Qt User Interfaces
A decision guide for Qt interfaces: when QML (Qt Quick) beats QWidget, the constraints to check first, and two minimal button examples with validation steps.
25 Apr 2026, 21:35 UTC

The decision and the takeaway
You are starting a Qt application, or adding a new front end to an existing C++ core, and must choose between QML (Qt Quick) and QWidget. The practical rule: choose QML when the interface is animated, touch-driven, or expected to change layout frequently; choose QWidget for form-heavy desktop tools that must run on hardware with uncertain graphics support and that integrate closely with existing C++ code. Both stacks are supported in Qt 5 and Qt 6, so this is an engineering trade-off, not a modern-versus-legacy choice.
Constraints to check first
- Graphics stack: Qt Quick renders through a GPU-accelerated scene graph. In Qt 6 this goes through the RHI abstraction over OpenGL, Vulkan, Metal, or Direct3D. A software renderer exists (set the environment variable
QT_QUICK_BACKEND=software) for GPU-less targets, but animated UIs will be slow on it. QWidget paints with Qt's raster engine and needs no GPU. - Module availability: the QML example below uses Qt Quick Controls. If your project depends on Qt Labs modules, remember they carry no compatibility guarantee, so confirm they exist in your target Qt version before upgrading.
- Footprint: QML applications link the QML engine and a JavaScript runtime, so binaries and runtime memory grow. QWidget applications link only the widgets library.
- Existing code: a mature QWidget code base rarely benefits from a rewrite. Mixed applications are possible with
QQuickWidget, but all UI updates must stay on the GUI thread.
Side-by-side comparison
| Aspect | QML (Qt Quick) | QWidget |
|---|---|---|
| UI definition | Declarative .qml files loaded at runtime; UI tweaks need no C++ recompile | C++ code or Qt Designer .ui files compiled in |
| Animation | Property bindings, behaviors, and particles built in | QPropertyAnimation and QGraphicsEffect; more manual work |
| Touch input | Flickable, PinchArea, and multi-touch events are first-class | Basic touch events; gestures require extra handling |
| C++ integration | Context properties or registered types; signals and slots cross the boundary | Direct C++ inheritance; no binding layer |
| Rendering | Scene graph via RHI in Qt 6, with a software fallback | Raster painting; no GPU requirement |
| Footprint | Larger: QML engine plus JavaScript runtime | Smaller: widgets library only |
Trade-offs in practice
QML keeps layout and visual logic next to the visuals they describe, and small UI changes do not require recompiling C++. Animations that take dozens of lines with widgets are a few lines of QML. The costs are a JavaScript runtime to ship and start, performance that depends on the graphics stack, and debugging that spans two languages.
QWidget keeps everything in C++, works directly with the model/view classes, and runs anywhere Qt runs, including machines without usable GPU drivers. The costs are verbose layout code (unless you lean on .ui files), manual animation, and styling through Qt style sheets rather than QML's theming system.
The same button in both stacks
Both examples implement identical behavior: a button whose click reaches a C++ handler. They are written for Qt 6.x and were not executed for this article, so expect small adjustments (import versions, file paths) on Qt 5.15.
QML front end
// qml-main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>
class Backend : public QObject {
Q_OBJECT
public slots:
void handleClick() { qDebug() << "Button clicked from QML"; }
};
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
Backend backend;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("backend", &backend);
engine.load(QUrl::fromLocalFile(QStringLiteral("main.qml")));
if (engine.rootObjects().isEmpty())
return 1;
return app.exec();
}
#include "main.moc" // needed because Q_OBJECT appears in a .cpp file
// main.qml, placed next to the executable for this demo
import QtQuick
import QtQuick.Controls
ApplicationWindow {
visible: true
width: 400
height: 300
title: "QML demo"
Button {
anchors.centerIn: parent
text: "Press me"
onClicked: backend.handleClick()
}
}
The context property must be registered before engine.load(), which is why backend is constructed first. Context properties are the quickest bridge for one or two objects; larger applications should register proper QML types instead.
QWidget front end
// widget-main.cpp
#include <QApplication>
#include <QPushButton>
#include <QDebug>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QPushButton button("Press me");
QObject::connect(&button, &QPushButton::clicked, [] {
qDebug() << "Button clicked from QWidget";
});
button.resize(200, 80);
button.show();
return app.exec();
}
Building either example
Run the following commands in the project directory. You need write access there, a Qt 6 installation, and a CMake version that meets your Qt release's requirement. Replace the prefix path with your Qt install, for example /opt/Qt/6.7.2/gcc_64 or C:/Qt/6.7.2/msvc2019_64.
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/opt/Qt/6.7.2/gcc_64
cmake --build .
In CMakeLists.txt, use find_package(Qt6 REQUIRED COMPONENTS Quick) and set(CMAKE_AUTOMOC ON) for the QML target (production code should embed main.qml in resources with qt_add_qml_module), and find_package(Qt6 REQUIRED COMPONENTS Widgets) for the widget target. Expected result: a window opens and each click prints the matching qDebug line. On Windows, read the output in Qt Creator's Application Output pane, because GUI binaries do not attach a console by default.
How to validate the choice on your hardware
- Startup: on Linux or macOS run
time ./qml-demoandtime ./widget-demoseveral times and compare; on Windows, time the launches with a script. Expect the QML binary to start slower; the size of the gap tells you whether it matters. - Memory: record peak resident memory with heaptrack or Valgrind's massif on Linux, Instruments on macOS, or Visual Studio's diagnostic tools on Windows.
- Responsiveness: add a continuously animated element to each version, a Rectangle with a SequentialAnimation in QML or a QPropertyAnimation on a widget, and watch for dropped frames while resizing or under CPU load. The QML version is the one most sensitive to weak GPUs.
- Worst-case target: run both binaries on the least capable machine you must support. If the QML app cannot create a graphics context, retry with
QT_QUICK_BACKEND=software; if that is still too slow, that target is a QWidget candidate. On Linux,glxinfo | grep OpenGL(from the mesa-utils package) shows the desktop GL version.
Limitations
- The snippets target Qt 6.x and were not executed for this article; on Qt 5.15 you need versioned imports such as
import QtQuick 2.15. - Context properties bypass compile-time checking and are best limited to one or two objects; registered QML types scale better.
- Startup and memory figures quoted anywhere are platform- and version-dependent; base your decision on measurements from your own targets.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.