Qt Quick for Embedded Linux
Goal: Understand how Qt Quick applications are structured, how QML and C++ interact, and how to develop embedded UIs using the EGLFS platform plugin.
Related Tutorials
For hands-on practice, see: Qt + EGLFS Dashboard | Qt App Launcher
For graphics stack context and Qt vs SDL2 comparison, see: Graphics Stack
1. Why Qt on Embedded?
SDL2 teaches you exactly what the hardware does — you open a DRM surface, draw every pixel, and handle every touch event. This is valuable for learning and for performance-critical custom rendering. But when a product needs forms, buttons, lists, swipe gestures, animations, and text layout, reimplementing all of that in SDL2 becomes the bottleneck.
Qt provides these as built-in components. The trade-off is a larger runtime (~30-80 MB RAM vs ~5 MB for SDL2) and a steeper learning curve. On a Raspberry Pi 4 with 1-4 GB RAM, the runtime cost is acceptable; on a 64 MB microcontroller SoC, it is not.
When to use Qt: - Multi-screen or multi-page UI (settings, dashboards, launchers) - Touch gestures beyond simple tap (swipe, pinch, long-press) - Text-heavy UIs with internationalization - Rapid prototyping — QML changes without recompiling C++
When to stay with SDL2: - Single custom-drawn display (gauges, plots, games) - Minimal RAM budget (<64 MB) - Learning how graphics hardware works - Maximum control over every frame
2. Architecture: QML + C++
A Qt Quick application splits into two layers:
┌─────────────────────────────────────────┐
│ QML (declarative) │
│ - UI layout, components, animations │
│ - Property bindings (automatic updates)│
│ - Touch/mouse input handling │
└────────────────┬────────────────────────┘
│ Q_PROPERTY signals
│ Q_INVOKABLE methods
┌────────────────┴────────────────────────┐
│ C++ (imperative) │
│ - Hardware access (sysfs, /dev, /proc) │
│ - Business logic, processes, timers │
│ - System calls, file I/O │
└─────────────────────────────────────────┘
QML describes what the UI looks like. It is a declarative language — you state relationships ("this text shows the temperature") and the engine tracks dependencies and redraws automatically.
C++ handles how data is obtained. It reads sensors, manages processes, and exposes values to QML through Qt's property system.
The connection between them uses three mechanisms:
Q_PROPERTY — Data from C++ to QML
class Backend : public QObject {
Q_OBJECT
Q_PROPERTY(float temperature READ temperature NOTIFY temperatureChanged)
public:
float temperature() const { return m_temperature; }
signals:
void temperatureChanged();
};
In QML, the property is used by name:
When the C++ side emits temperatureChanged(), QML automatically re-evaluates every expression that uses backend.temperature. No manual refresh, no callback registration.
Q_INVOKABLE — Actions from QML to C++
Called from QML like a JavaScript function:
setContextProperty — Connecting the two
In main(), the C++ object is made available to QML by name:
Every QML file can then reference backend.temperature, backend.launch(), etc.
3. QML Essentials
Components and Properties
Every QML file defines a reusable component. A component is a tree of visual items with typed properties:
// AppButton.qml
Item {
property string label: ""
property color accent: "#50b4ff"
signal clicked()
Rectangle {
color: accent
Text { text: label }
MouseArea { onClicked: parent.parent.clicked() }
}
}
Used elsewhere:
This is the same pattern as creating a class with public members in C++ — but the "class" is a .qml file and the "members" are property declarations.
Property Bindings
The most important concept in QML. A binding is not an assignment — it is a live relationship:
Rectangle {
width: parent.width * 0.5 // always half the parent, even after resize
height: width // always square
color: slider.value > 50 ? "red" : "blue" // reactive to slider
}
Bindings are re-evaluated automatically when any dependency changes. This eliminates the update/redraw logic that dominates SDL2 applications.
Layouts
Qt Quick provides RowLayout, ColumnLayout, and GridLayout for responsive positioning:
GridLayout {
columns: 3
AppButton { Layout.fillWidth: true; Layout.fillHeight: true }
AppButton { Layout.fillWidth: true; Layout.fillHeight: true }
// ...
}
Items automatically resize when the window or parent changes size. Compare this to SDL2, where you calculate positions manually: x = margin + col * (btn_w + gap).
SwipeView and Navigation
For multi-page UIs, SwipeView handles gesture-based page switching:
import QtQuick.Controls
SwipeView {
id: swipe
Item { /* Page 1 content */ }
Item { /* Page 2 content */ }
Item { /* Page 3 content */ }
}
PageIndicator {
count: swipe.count
currentIndex: swipe.currentIndex
}
SwipeView provides velocity tracking, deceleration curves, and snap-to-page with zero custom code. The equivalent in SDL2 would require ~80 lines of touch event processing, velocity calculation, and animation logic.
Canvas — Custom Drawing
When built-in components are not enough, Canvas provides an HTML5 Canvas-style API:
Canvas {
onPaint: {
var ctx = getContext("2d")
ctx.beginPath()
ctx.arc(width/2, height/2, 50, 0, Math.PI * 2)
ctx.fillStyle = "#00c878"
ctx.fill()
}
}
This is useful for custom gauges, charts, or visualizations. Trigger repaints with requestPaint() when data changes.
4. EGLFS: Qt Without a Compositor
On a desktop, Qt uses the platform's window system (X11 or Wayland). On an embedded device with no compositor, Qt uses EGLFS — a platform plugin that renders directly to the GPU via EGL and KMS/DRM.
Desktop: Embedded:
┌──────────┐ ┌──────────┐
│ Qt App │ │ Qt App │
└────┬─────┘ └────┬─────┘
│ │
┌────┴─────┐ ┌────┴──────┐
│ Wayland/ │ │ EGLFS │
│ X11 │ │ plugin │
└────┬─────┘ └────┬──────┘
┌────┴─────┐ │
│Compositor│ │
└────┬─────┘ │
┌────┴─────┐ ┌────┴──────┐
│ DRM/KMS │ │ DRM/KMS │
└──────────┘ └───────────┘
Key implications:
- One app at a time. EGLFS renders fullscreen with no window management. Only one EGLFS application can hold DRM master. To run multiple apps, you need a launcher pattern (hide window → start child → show window on exit).
- Same QML code. The application code is identical on desktop and embedded. Only the
QT_QPA_PLATFORMenvironment variable changes:xcbfor desktop testing,eglfsfor target. - Touch works automatically. EGLFS reads
/dev/input/event*devices directly. Touchscreens are mapped to the display surface without additional configuration.
Running on target
# Direct execution
sudo QT_QPA_PLATFORM=eglfs ./my_app
# Or set in the systemd service
Environment=QT_QPA_PLATFORM=eglfs
Useful EGLFS environment variables
| Variable | Effect |
|---|---|
QT_QPA_PLATFORM=eglfs |
Use EGLFS backend |
QT_QPA_EGLFS_KMS_CONFIG=/path/to/config.json |
Custom KMS configuration (connector, mode) |
QT_QPA_EGLFS_ROTATION=180 |
Rotate output (0, 90, 180, 270) |
QT_QPA_EGLFS_PHYSICAL_WIDTH=108 |
Override physical display width in mm (for DPI) |
QT_QPA_EGLFS_PHYSICAL_HEIGHT=65 |
Override physical display height in mm |
QT_LOGGING_RULES="qt.qpa.*=true" |
Debug EGLFS initialization |
5. Build System
Qt6 uses CMake. The minimal pattern for a QML application:
cmake_minimum_required(VERSION 3.16)
project(my_app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # auto-generate MOC for Q_OBJECT classes
find_package(Qt6 REQUIRED COMPONENTS Core Quick)
qt_add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE Qt6::Core Qt6::Quick)
# Embed QML files into the binary (no file path lookups at runtime)
qt_add_qml_module(my_app
URI MyApp
VERSION 1.0
QML_FILES Main.qml MyComponent.qml
)
Key points:
CMAKE_AUTOMOC ON— Scans C++ files forQ_OBJECTmacros and generates Meta-Object Compiler (MOC) code automatically. Without this, signals, slots, and properties do not work.qt_add_qml_module— Embeds QML files into the binary as a Qt resource. TheURIis howmain.cpploads the module:engine.loadFromModule("MyApp", "Main").#include "main.moc"— When all C++ is in a single file, this line at the bottom includes the auto-generated MOC output. Multi-file projects do not need this.
Host vs target build
The same CMakeLists.txt works on both:
# On host (desktop testing)
cmake -B build && cmake --build build
QT_QPA_PLATFORM=xcb ./build/my_app
# On target (Pi with Qt6 installed)
cmake -B build && cmake --build build
sudo QT_QPA_PLATFORM=eglfs ./build/my_app
For cross-compilation (building on host for target), see the Buildroot Qt6 package or use a Qt cross-compilation toolchain file.
6. Development Workflow
Step 1: Develop on host
Build and test on your development machine using xcb (X11) or wayland platform. The window appears on your desktop. This is fast — no deploy step, instant feedback.
Step 2: Deploy to target
Copy source to the Pi, build natively, and run with QT_QPA_PLATFORM=eglfs. Fix any EGLFS-specific issues (touch mapping, DPI, rotation).
Step 3: Iterate
QML files embedded via qt_add_qml_module require a rebuild to update. For faster iteration during development, you can temporarily load QML from the filesystem instead:
// Development: load from file (editable without rebuild)
engine.load(QUrl::fromLocalFile("Main.qml"));
// Production: load from embedded resource
engine.loadFromModule("MyApp", "Main");
Common patterns
| Pattern | How |
|---|---|
| Read a sensor | C++ QTimer polls sysfs/devfs, exposes value as Q_PROPERTY |
| Launch external process | C++ QProcess with finished() signal for async notification |
| Navigate between pages | QML SwipeView or StackView |
| Reusable UI component | Separate .qml file with property and signal declarations |
| Custom drawing | QML Canvas with getContext("2d") |
| Animations | QML Behavior on / NumberAnimation / PropertyAnimation |
| Keyboard fallback | QML Keys.onUpPressed / Keys.onEscapePressed |
| Persistent settings | C++ QSettings (reads/writes INI file) |
7. Qt vs SDL2 — When to Switch
You have built applications with both toolkits in this course. Here is a decision framework:
flowchart TD
A[New embedded UI project] --> B{Complex UI?<br/>Buttons, lists, pages,<br/>text input, gestures}
B -->|Yes| C{RAM > 64 MB?}
B -->|No| D[SDL2 or raw DRM]
C -->|Yes| E[Qt + EGLFS]
C -->|No| D
E --> F{Custom rendering<br/>needed too?}
F -->|Yes| G[Qt + embedded<br/>OpenGL Canvas]
F -->|No| H[Pure QML]
The key insight: Qt and SDL2 are not competing alternatives — they solve different problems. SDL2 is a rendering surface; Qt is a UI framework. Many production systems use both (Qt for chrome, custom rendering for data visualization).
Further Reading
- Qt for Device Creation — Qt's official embedded Linux documentation
- QML Book — comprehensive QML tutorial
- EGLFS platform plugin — configuration reference
- Qt Quick Controls — built-in buttons, sliders, dialogs