Skip to content

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:

Text { text: backend.temperature.toFixed(1) + " °C" }

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++

Q_INVOKABLE void launch(const QString &command) { /* ... */ }

Called from QML like a JavaScript function:

Button { onClicked: backend.launch("my-app") }

setContextProperty — Connecting the two

In main(), the C++ object is made available to QML by name:

Backend backend;
engine.rootContext()->setContextProperty("backend", &backend);

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:

AppButton { label: "Doom"; accent: "#e05050"; onClicked: doSomething() }

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_PLATFORM environment variable changes: xcb for desktop testing, eglfs for 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 for Q_OBJECT macros 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. The URI is how main.cpp loads 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