Skip to content
Back to postfabrica
Research2026-07-14·18 min read

C++/Qt/QML Patterns & Best Practices

We scanned 200+ open-source Qt repositories across GitHub, GitLab, KDE's GitLab, and Bitbucket. From CMake setup to QML architecture, testing strategies to CI/CD pipelines — here are the patterns that emerged from reading actual production code, not documentation pages.

329
Total repos
4
Platforms
9
Deep analyses
17
Topics

Project Structure & Layout

High-star Qt repositories reveal a converging standard. We read the CMakeLists.txt and directory structures of moonlight-qt (⭐17.8k), shotcut (⭐14.5k), and KDE/krita (⭐10k). All three follow the same fundamental principles despite vastly different codebase sizes.

moonlight-qt organizes by functional module:backend/,gui/,streaming/,settings/. shotcut organizes by Qt role:dialogs/,docks/,models/,widgets/. krita takes it further with a library-per-directory approach: each libs/ subdirectory is a separate shared library.

my-qt-app/
├── CMakeLists.txt              # Root build file
├── src/
│   ├── core/                   # Business logic, data models
│   │   ├── CMakeLists.txt
│   │   ├── datamanager.cpp
│   │   └── datamanager.h
│   ├── ui/                     # C++ UI controllers, widget wrappers
│   │   ├── CMakeLists.txt
│   │   ├── mainwindow.cpp
│   │   └── mainwindow.h
│   ├── qml/                    # QML files
│   │   ├── Main.qml
│   │   ├── pages/
│   │   │   ├── HomePage.qml
│   │   │   └── SettingsPage.qml
│   │   └── components/
│   │       └── StyledButton.qml
│   └── qmltypes/               # C++ classes registered for QML
├── tests/                      # Test files
│   ├── CMakeLists.txt
│   ├── unit/
│   └── integration/
├── resources/                  # Icons, fonts, translations
│   ├── icons/
│   ├── translations/
│   └── resources.qrc
├── docs/
└── .github/workflows/          # CI/CD

Key observations from top repositories

📁

Functional directory layout

Group sources by what they do — dialogs, models, controllers — not by file type. No separate include/ vs src/ split.

📄

Co-locate .cpp + .h

Headers and implementations sit side-by-side in the same directory. This is universal across all three projects.

🔧

Per-directory CMakeLists

Each module has its own CMakeLists.txt for modular builds. Root file uses add_subdirectory() to compose them.

📦

Vendored dependencies

moonlight-qt uses git submodules, shotcut uses add_subdirectory(CuteLogger), krita has a 3-tier system: 3rdparty/, 3rdparty_vendor/, 3rdparty_plugins/.

🔌

Plugin architecture at scale

Krita uses runtime-loadable plugin DLLs for paintops, tools, and filters. moonlight-qt keeps everything monolithic. Plugins pay off when extensibility matters.

💡

Dev-mode QML symlinks (from shotcut)

shotcut symlinks QML directories into the build tree using file(CREATE_LINK ...). This enables live QML editing during development without reinstalling resources — a pattern worth adopting for any QML-heavy project.

Build Systems: CMake Won

Across all 200+ analyzed repositories, the result is unambiguous: CMake has become the standard for Qt projects. QMake survives only in legacy/maintenance projects (moonlight-qt being the notable exception). Modern Qt6 projects have universally adopted CMake, and Qt's own build system migrated to CMake in Qt 6.0.

The standard Qt6 + CMake template

This is the pattern used by Serial-Studio, Librum-Reader, ApkStudio, and most modern Qt6 projects. Note the component-based find_package,qt_standard_project_setup(), and qt_add_executable (not plain add_executable).

Qt6 + CMake standard template
cmake_minimum_required(VERSION 3.16)

project(MyApp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Auto-run moc, uic, rcc
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

# Component-based Qt discovery
find_package(Qt6 6.2 REQUIRED COMPONENTS
    Core Gui Widgets Network Sql
)

# Qt6 standard project setup
qt_standard_project_setup()

# Executable
qt_add_executable(myapp
    src/main.cpp
    src/mainwindow.cpp
    src/mainwindow.h
)

target_link_libraries(myapp PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::Widgets
    Qt6::Network
)

# Install rules
install(TARGETS myapp
    BUNDLE DESTINATION .
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
)
QML module — compile-time resource embedding (Qt6)
qt_add_qml_module(myapp
    URI MyApp
    VERSION 1.0
    QML_FILES
        Main.qml
        pages/HomePage.qml
        pages/SettingsPage.qml
        components/StyledButton.qml
    RESOURCES
        images/logo.png
)
Platform-specific source inclusion (from shotcut)
if(WIN32)
    target_sources(myapp PRIVATE
        widgets/d3dvideowidget.cpp
        widgets/d3dvideowidget.h
    )
elseif(APPLE)
    target_sources(myapp PRIVATE
        widgets/metalvideowidget.mm
        widgets/metalvideowidget.h
    )
else()
    target_sources(myapp PRIVATE
        widgets/openglvideowidget.cpp
        widgets/openglvideowidget.h
    )
endif()
Automatic translation embedding (from KDE projects)
qt_add_translations(myapp
    TS_FILES
        translations/myapp_en.ts
        translations/myapp_de.ts
        translations/myapp_tr.ts
)
Per-module CMakeLists with add_subdirectory (from krita)
# Root CMakeLists.txt
add_subdirectory(libs)
add_subdirectory(plugins)
add_subdirectory(krita)

Additional CMake patterns from real repos

  • CMakePresets.json (shotcut) — standardized build configurations for IDE integration
  • Config headers (krita) — 25+ config-*.h.cmake files for compile-time feature detection
  • Code quality targets (shotcut) — clang-format, qmlformat, codespell as CMake custom targets
  • Dual Qt5/Qt6 (krita) — QT_MAJOR_VERSION variable parameterizes all find_package and target_link_libraries calls
  • Private Qt headers (krita) — Qt6GuiPrivate, Qt6CorePrivate for platform-specific features
  • ECM (KDE projects) — KDE's Extra CMake Modules provides macros, install paths, and tooling

C++ Architecture Patterns

01

The QObject + Pimpl Pattern

The most pervasive pattern across professional Qt codebases. Qt itself uses it, KDE Frameworks uses it, Stellarium uses it, Serial-Studio uses it. Private members are moved out of the header into a forward-declared Impl class, giving you ABI stability (changing private members doesn't require recompilation of consumers) and faster compile times (fewer include dependencies in the header).

// datamanager.h — PIMPL pattern
class DataManager : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString status READ status NOTIFY statusChanged)

public:
    explicit DataManager(QObject *parent = nullptr);
    ~DataManager();

    QString status() const;

public slots:
    void loadData(const QString &path);
    void saveData(const QString &path);

signals:
    void statusChanged();
    void dataLoaded(const QVector &data);

private:
    class Impl;
    std::unique_ptr d;  // private implementation
};

// datamanager.cpp
class DataManager::Impl {
public:
    QString status;
    QVector data;
};

DataManager::DataManager(QObject *parent)
    : QObject(parent)
    , d(std::make_unique())
{
    d->status = QStringLiteral("Ready");
}

DataManager::~DataManager() = default;

// All method implementations use d-> ...
02

Signal/Slot Architecture

Professional Qt codebases exclusively use the pointer-based connection syntax. The old string-based SIGNAL() / SLOT() macros produce runtime errors instead of compile-time errors. Three key conventions: use explicit connections for clarity, pass context objects for lifetime safety in lambdas, and specify Qt::QueuedConnection for cross-thread communication.

// GOOD — compile-time checked, type-safe
connect(m_worker, &Worker::progressChanged,
        m_progressBar, &QProgressBar::setValue);

// GOOD — lambda with context object for lifetime safety
// Auto-disconnects when 'this' is destroyed
connect(m_networkManager, &QNetworkAccessManager::finished,
        this, [this](QNetworkReply *reply) {
    handleResponse(reply);
    reply->deleteLater();
});

// GOOD — explicit QueuedConnection for cross-thread
connect(m_worker, &Worker::resultReady,
        this, &MainWindow::handleResult,
        Qt::QueuedConnection);

// BAD — old string-based syntax (runtime errors, no type checking)
// connect(m_worker, SIGNAL(progressChanged(int)),
//         m_progressBar, SLOT(setValue(int)));
03

Ownership and Parent-Child Hierarchy

Qt's memory management is built on parent-child ownership. When a QObject is deleted, all its children are automatically deleted too. This eliminates most manual memory management — but only if you actually set the parent. For non-QObject heap objects, use smart pointers.

// GOOD — parent manages child lifetime automatically
auto *button = new QPushButton(tr("Click Me"), this);  // this = parent
auto *timer = new QTimer(this);                         // auto-deleted

// GOOD — QScopedPointer / std::unique_ptr for non-QObject heap objects
auto parser = std::make_unique();

// GOOD — std::shared_ptr for shared non-Qt resources
auto textureCache = std::make_shared();

// BAD — bare new without parent or smart pointer
auto *widget = new QWidget();  // MEMORY LEAK if not parented
04

Singleton Pattern (Qt-style)

Used for global services like settings managers, theme providers, and application controllers. The Meyers singleton pattern (static local variable) is thread-safe in C++11+ and integrates cleanly with Qt's object model.

// settingsmanager.h — Qt-style singleton
class SettingsManager : public QObject {
    Q_OBJECT
public:
    static SettingsManager &instance();

    // Delete copy/move
    SettingsManager(const SettingsManager &) = delete;
    SettingsManager &operator=(const SettingsManager &) = delete;

    Q_INVOKABLE QString value(const QString &key) const;
    Q_INVOKABLE void setValue(const QString &key, const QString &value);

private:
    SettingsManager();
    QSettings m_settings;
};

// settingsmanager.cpp
SettingsManager &SettingsManager::instance() {
    static SettingsManager inst;   // Meyers singleton
    return inst;
}
05

Controller / Service / Model Layering

Observed in Serial-Studio, Librum-Reader, and modern Qt projects. Each layer depends only on the layer below it. The view layer (QML or Widgets) knows nothing about databases or business logic. The controller mediates between UI and services. Services contain business logic. Models hold data.

QML / Widgets UIView layer — declarative
ViewController (QObject)Controller — mediator
Service / Manager (QObject)Business logic
Model / Data LayerDB, files, network

QML Architecture & Patterns

Analyzed from FluentUI (⭐4.5k, 90+ QML components), quickshell (⭐2.6k, Wayland desktop shell), and KDE Plasma. Modern QML applications follow a component-first hierarchy with clear separation between pages, reusable components, and theme definitions.

Qt6 declarative type registration

With Qt6's QML_ELEMENT,QML_SINGLETON, andqt_add_qml_module in CMake, manual qmlRegisterType() calls are a thing of the past. Just annotate your C++ headers and CMake handles the rest. FluentUI maintains dual Qt5/Qt6 support — the Qt6 path uses macros, the Qt5 path has ~200 lines of manual registration.

QML_ELEMENT and QML_SINGLETON (Qt6)

// datacontroller.h — Qt6 declarative registration
class DataController : public QObject {
    Q_OBJECT
    QML_ELEMENT          // Exposed as DataController in QML
    QML_SINGLETON        // As a singleton

    Q_PROPERTY(QString status READ status NOTIFY statusChanged)
    Q_PROPERTY(int count READ count NOTIFY countChanged)

public:
    explicit DataController(QObject *parent = nullptr);

    QString status() const;
    int count() const;

    Q_INVOKABLE void loadData(const QUrl &fileUrl);
    Q_INVOKABLE QVariantMap getData(int index) const;
    Q_INVOKABLE void addItem(const QString &name);

signals:
    void statusChanged();
    void countChanged();
    void dataLoaded();
};

// No manual qmlRegisterType needed!
// The QML_ELEMENT macro + CMake qt_add_qml_module handles everything

Legacy singleton registration (Qt5)

// singleton registration (Qt5 legacy / manual)
qmlRegisterSingletonType(
    "MyApp.Core", 1, 0, "Settings",
    [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
        Q_UNUSED(engine);
        Q_UNUSED(scriptEngine);
        return &SettingsManager::instance();
    }
);

// In QML:
// import MyApp.Core 1.0
// Text { text: Settings.value("username") }

Enum exposure to QML

// C++ enum exposure to QML
namespace FluPageType {
    Q_NAMESPACE
    QML_NAMED_ELEMENT(FluPageType)

    enum Type {
        Single,
        Multi,
        Tabbed
    };
    Q_ENUM_NS(Type)
};

// In QML:
// import MyApp.Core 1.0
// FluPage {
//     pageType: FluPageType.Single
// }

Declarative property bindings

The fundamental QML principle: bindings are reactive, not imperative. Whenever you write Component.onCompletedto set a property, you're probably doing it wrong. The QML engine tracks dependencies between bindings and automatically re-evaluates them when dependencies change.

Bad — imperative

// BAD — imperative property setting
Item {
    Component.onCompleted: {
        rect.width = parent.width / 2
        rect.color = "red"
    }
}

Good — declarative binding

// GOOD — declarative binding
Item {
    Rectangle {
        width: parent.width / 2
        color: "red"
    }
}

Custom components

From cool-retro-term and FluentUI: inherit from QtQuick.Controls types and overridebackground andcontentItem. UseBehavior on for smooth animations.

StyledButton.qml — full custom component
// StyledButton.qml — custom component pattern
import QtQuick 2.15
import QtQuick.Controls 2.15

Button {
    id: control

    // Public properties
    property string accentColor: "#0078d7"
    property bool : false

    implicitHeight: 36
    padding: 12

    contentItem: Text {
        text: control.text
        font: control.font
        color: control.enabled ? "white" : "#888"
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        elide: Text.ElideRight
    }

    background: Rectangle {
        radius: control. ? height / 2 : 4
        color: !control.enabled ? "#444"
              : control.pressed ? Qt.darker(control.accentColor, 1.3)
              : control.hovered ? Qt.lighter(control.accentColor, 1.1)
              : control.accentColor

        border.width: !control.enabled ? 0 : 1
        border.color: control.accentColor

        Behavior on color { ColorAnimation { duration: 150 } }
    }
}
💡

Hot-reload architecture (from quickshell)

quickshell implements a production-grade QML hot-reload system: a Reloadable base class that all shell types inherit from, a RootWrapper that watches the filesystem and destroys/recreates the entire QML engine context, and a reloadableId property for state preservation across reloads. Worth studying if you build any QML-heavy application.

C++/QML Integration

FluentUI uses a custom Q_PROPERTY_AUTO macro that generates Q_PROPERTY + getter + setter + signal automatically. quickshell favorsQ_PROPERTY(... MEMBER mField) syntax. Both approaches reduce boilerplate — the choice depends on whether you need custom logic in getters/setters.

Model/View pattern

The dominant pattern across all modern Qt6+QML codebases: subclassQAbstractListModel, expose it withQML_ELEMENT, define custom roles via an enum + roleNames(), and provide Q_INVOKABLE methods for mutations. The QML side just binds to model.roleName.

C++ model exposed to QML

// tasklistmodel.h — C++ model for QML
class TaskListModel : public QAbstractListModel {
    Q_OBJECT
    QML_ELEMENT

public:
    enum Roles {
        TitleRole = Qt::UserRole + 1,
        DescriptionRole,
        PriorityRole,
        CompletedRole
    };
    Q_ENUM(Roles)

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QHash roleNames() const override {
        return {
            {TitleRole, "title"},
            {DescriptionRole, "description"},
            {PriorityRole, "priority"},
            {CompletedRole, "completed"}
        };
    }

    Q_INVOKABLE void addTask(const QString &title, const QString &description);
    Q_INVOKABLE void removeTask(int index);
    Q_INVOKABLE void toggleTask(int index);

private:
    QVector m_tasks;
};
// QML usage of C++ model
ListView {
    model: TaskListModel { id: taskModel }
    delegate: TaskDelegate {
        title: model.title
        description: model.description
        priority: model.priority
        completed: model.completed
        onToggle: taskModel.toggleTask(index)
    }
    // ...
}

Proxy models for filtering and sorting

Don't duplicate filtering logic. Subclass QSortFilterProxyModel, expose a Q_PROPERTY filter text, and override filterAcceptsRow(). Krita's layer models and KDE file managers use this extensively.

QSortFilterProxyModel with QML bindings
// SortedTaskModel — filter + sort proxy
class SortedTaskModel : public QSortFilterProxyModel {
    Q_OBJECT
    QML_ELEMENT

    Q_PROPERTY(QString filterText READ filterText
               WRITE setFilterText NOTIFY filterTextChanged)

public:
    bool filterAcceptsRow(int sourceRow,
        const QModelIndex &sourceParent) const override;

    QString filterText() const { return m_filterText; }
    void setFilterText(const QString &text) {
        if (m_filterText == text) return;
        m_filterText = text;
        invalidateFilter();
        emit filterTextChanged();
    }

signals:
    void filterTextChanged();

private:
    QString m_filterText;
};
💡

Not everything needs QML

nodeeditor (⭐3.7k) uses zero QML. Complex 2D graphics editing is better served byQGraphicsView with a clean C++ model-view architecture. The project even uses JSON-based runtime styling (DefaultStyle.json) so you can change colors and widths without recompiling. If your "QML" would be mostly Canvas and imperative drawing, use QGraphicsView instead.

Testing & CI/CD

We analyzed the test suites and CI pipelines of qTox (⭐5k, Qt5), Serial-Studio (⭐7k, Qt6), and ApkStudio (⭐4.3k, Qt6). The key finding: test framework choice correlates with architecture. Monolithic C++ Qt apps use QtTest. Apps with an API surface use pytest black-box. Small tools may skip tests entirely.

ProjectFrameworkApproachCI
qToxQtTest + CTest20+ test binaries, per-module10 Docker images, ASAN
Serial-Studiopytest (Python)Black-box API testing on port 7777PGO, security fuzzing, CVE scan
ApkStudioNoneinstall-qt-action, basic

QtTest — the standard framework

qTox is the gold standard for C++ unit testing with QtTest. A customauto_test() CMake function compiles each test/<subsystem>/<module>_test.cppinto a separate binary, links it against the static app library + QtTest + mock library, and registers it with CTest. 20+ test binaries across 9 subsystem directories.

QtTest unit test with data-driven testing + QSignalSpy
#include 

class TestDataManager : public QObject {
    Q_OBJECT
private slots:
    void initTestCase();
    void cleanupTestCase();
    void testLoadData();
    void testSaveData_data();    // Data-driven testing
    void testSaveData();
};

void TestDataManager::testLoadData() {
    DataManager mgr;
    QSignalSpy spy(&mgr, &DataManager::dataLoaded);
    mgr.loadData(":/test_data/sample.json");
    QVERIFY(spy.wait(1000));  // Wait for async signal
    QCOMPARE(mgr.status(), QStringLiteral("Loaded"));
}

void TestDataManager::testSaveData_data() {
    QTest::addColumn("filename");
    QTest::addColumn("expected");
    QTest::newRow("valid") << "test.json" << true;
    QTest::newRow("invalid") << "" << false;
}

void TestDataManager::testSaveData() {
    QFETCH(QString, filename);
    QFETCH(bool, expected);
    DataManager mgr;
    QCOMPARE(mgr.saveData(filename), expected);
}

CMake test integration

enable_testing()

add_executable(test_datamanager
    tests/test_datamanager.cpp
)
target_link_libraries(test_datamanager PRIVATE
    Qt6::Test
    myapp_core
)

# Register with CTest
add_test(NAME datamanager COMMAND test_datamanager)

Signal testing with QSignalSpy

// Signal testing with QSignalSpy
QSignalSpy spy(model, &TaskListModel::rowsInserted);
model.addTask("New Task", "Description");
QCOMPARE(spy.count(), 1);

// Verify arguments
QCOMPARE(spy.at(0).at(1).toInt(), 0);  // first row
QCOMPARE(spy.at(0).at(2).toInt(), 0);  // last row
QML testing with QtTest
import QtTest 1.15

TestCase {
    name: "StyledButtonTests"
    when: windowShown

    function test_button_click() {
        var btn = createTemporaryObject(component, window,
            {text: "Test"})
        verify(btn)
        compare(btn.text, "Test")
        btn.clicked()
        // Verify signal
    }
}

CI/CD for Qt projects

The simplest and most common approach is jurplel/install-qt-action@v4on GitHub Actions (used by ApkStudio and thousands of smaller projects). Serial-Studio uses a custom install script with Qt account credentials and caching. qTox goes all-in with 10 Docker images covering Debian, Ubuntu LTS, AlmaLinux, Fedora, openSUSE, Arch, Flatpak, and Windows cross-compile via MXE.

GitHub Actions: Install Qt + Build + Test
# .github/workflows/build.yml
name: Build

on: [push, pull_request]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        qt_version: ['6.7.*']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install Qt
        uses: jurplel/install-qt-action@v4
        with:
          version: ${{ matrix.qt_version }}
          modules: qtwebengine qt5compat qtmultimedia

      - name: Install Linux deps
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libgl1-mesa-dev libxkbcommon-dev \
            libwayland-dev libpulse-dev

      - name: Configure
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build
        run: cmake --build build --parallel

      - name: Test
        run: ctest --test-dir build --output-on-failure
Docker for reproducible builds
# Dockerfile for reproducible Qt builds
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    cmake g++ qt6-base-dev qt6-declarative-dev
COPY . /src
WORKDIR /src
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \
    cmake --build build --parallel

Advanced CI patterns from Serial-Studio

  • PGO (Profile-Guided Optimization) — two-stage build: GENERATE → training run → USE rebuild
  • Hardening flagsENABLE_HARDENING=ON, PRODUCTION_OPTIMIZATION=ON
  • CVE scanning — deglyph binary scan with SARIF → GitHub Security
  • Crash capture — core dumps + GDB backtrace (Linux), WER (Windows), DiagnosticReports (macOS)
  • Flaky test handling — curated _FLAKY_TESTS set with reruns=2
  • Code signing — GPG (Linux), Apple notarization + stapling (macOS)

QML Performance Optimization

From the Qt documentation and analysis of cool-retro-term, FluentUI, and quickshell. These are the seven patterns that matter most for responsive QML applications.

1. Avoid binding loops

A binding loop occurs when one property depends on another that depends back on the first. The QML engine detects these and prints warnings — they cause infinite re-evaluation.

// BAD — binding loop: width depends on height,
// height depends on width
Rectangle {
    width: height * 2
    height: width / 2  // ERROR: Binding loop detected
}

// GOOD — use a single source property
Rectangle {
    id: root
    property real baseSize: 100
    width: baseSize * 2
    height: baseSize
}

2. Keep delegates lightweight

From cool-retro-term and FluentUI: 20 bindings and 5 Loaders in a delegate is a recipe for jank. Push complexity to the C++ model. The delegate should only bind to model.roleName and do simple conditional styling.

3. Prefer ListView over Repeater for large lists

ListView recycles delegates. Repeater creates every item immediately.

// BAD for large data — Repeater creates ALL items
Repeater {
    model: 10000
    delegate: Item { ... }
}

// GOOD — ListView recycles delegates
ListView {
    model: 10000
    delegate: Item { ... }
    cacheBuffer: 200  // Pre-render slightly beyond viewport
}

4. Use Loader for conditional/heavy components

Lazy-load charts, editors, and other heavy components only when they become visible.

// Lazy load heavy components
Loader {
    sourceComponent: visible ? heavyChartComponent : null
}

Component {
    id: heavyChartComponent
    // Complex chart that only loads when needed
    ChartView { ... }
}

5. Don't create objects in bindings

If a binding calls a function that creates objects (e.g., returns a new array), every re-evaluation creates garbage. Bind to a C++ property that updates on change instead.

6. Accumulate before assigning

From Qt performance docs: incrementally updating a property in a loop triggers bindings on every iteration.

// BAD — triggers binding re-evaluation on each iteration
for (var i = 0; i < items.length; i++) {
    total += items[i].value  // each += triggers bindings
}

// GOOD — accumulate in local variable, assign once
var tempTotal = 0
for (var i = 0; i < items.length; i++) {
    tempTotal += items[i].value
}
total = tempTotal  // Single binding trigger

7. Use Qt.binding() sparingly for dynamic bindings

When a binding genuinely needs to be created at runtime (not at parse time), Qt.binding() is the tool. But prefer static declarative bindings whenever possible — they're faster to evaluate and easier to debug.

Threading & Async Programming

Three patterns dominate in professional Qt code. Use them in this order of preference:QtConcurrent::run for short one-off tasks,Worker object + moveToThread for long-running or stateful work,QFuture + .then() for composable async chains.

Worker object pattern (recommended for complex work)

Create a QObject worker, move it to a QThread, connect signals/slots. The thread's event loop handles queued method invocations. This is the pattern used by Serial-Studio for telemetry processing and by qTox for network operations.

Worker object + moveToThread
// worker.h — Worker object pattern
class ImageProcessor : public QObject {
    Q_OBJECT
public slots:
    void process(const QString &imagePath);

signals:
    void finished(const QImage &result);
    void error(const QString &message);
};

// In controller:
void Controller::startProcessing(const QString &path) {
    auto *worker = new ImageProcessor;
    worker->moveToThread(&m_workerThread);

    // Wire up signals
    connect(&m_workerThread, &QThread::finished,
            worker, &QObject::deleteLater);
    connect(worker, &ImageProcessor::finished,
            this, &Controller::handleResult);
    connect(this, &Controller::processImage,
            worker, &ImageProcessor::process);

    m_workerThread.start();
    emit processImage(path);  // Queued to worker thread
}

QFuture + .then() chaining (Qt6)

Qt6 introduced QFuture::then()for promise chaining without callback hell. For full async/await syntax, the third-party QCorolibrary is gaining traction in the Qt community.

QFuture chaining + QCoro async/await
#include 
#include 

QFuture loadImageAsync(const QString &path) {
    return QtConcurrent::run([path]() -> QImage {
        return QImage(path);
    });
}

// Qt6 .then() chaining
loadImageAsync("photo.png").then([](QImage image) {
    displayLabel->setPixmap(QPixmap::fromImage(image));
});

// Multiple async operations with QCoro (third-party)
QCoro::Task fetchData() {
    auto *reply = co_await m_network.get(request);
    auto data = reply->readAll();
    co_await processAsync(data);
    emit ready(data);
}

Resource Management

Qt's resource system (.qrc files) embeds images, fonts, translations, and QML files directly into the binary. In Qt6,qt_add_qml_module handles this automatically — no separate .qrc needed for QML. For large assets (videos, large images), use file paths resolved at runtime instead of embedding.

Qt resource file (.qrc)



    
        images/logo.png
        icons/app.ico
        fonts/roboto.ttf
        translations/app_en.qm
    

Access in C++ and QML

// Access in C++
QPixmap logo(QStringLiteral(":/images/logo.png"));
QFontDatabase::addApplicationFont(QStringLiteral(":/fonts/roboto.ttf"));

// Access in QML
// Image { source: "qrc:/images/logo.png" }

Deployment & Packaging

Each platform has its deployment tool. They all do the same thing: copy required Qt DLLs/frameworks/plugins next to your binary so it runs on a clean machine without Qt installed.

🐧 Linux

# Create AppImage from built binary
linuxdeployqt build/myapp -appimage \
    -qmake=$(which qmake) \
    -executable=build/myapp

🪟 Windows

# Auto-copy Qt DLLs and plugins
windeployqt --release --no-translations --no-opengl-sw build/myapp.exe

🍎 macOS

# Create .app bundle + DMG
macdeployqt build/myapp.app -dmg -always-overwrite

Modern Qt6 Idioms

Qt6 introduced several features that eliminate boilerplate and enable new patterns. If you're starting a new project, use all of these.

Bindable properties (no signals needed)

Qt6's QProperty<T> enables reactive bindings in pure C++ — the same concept as QML property bindings, but on the C++ side.

// Qt6 bindable properties — no signals needed
class Sensor : public QObject {
    Q_OBJECT
    Q_PROPERTY(double temperature READ temperature
               BINDABLE bindableTemperature)
public:
    QBindable bindableTemperature() { return &m_temperature; }
    double temperature() const { return m_temperature.value(); }
private:
    QProperty m_temperature{20.0};
};

// Usage — reactive binding in C++
sensor.bindableTemperature().setBinding([]() {
    return computeTemperature();
});

QStringView / QLatin1StringView

Pass read-only string parameters by value without allocation. QStringLiteral for compile-time string construction.

// Qt6 — prefer string views for read-only parameters
QString process(QStringView input);
bool match(QLatin1StringView pattern);

// Efficient string construction
QString msg = QStringLiteral("Hello %1").arg(name);

QCoro for async/await (third-party)

Write asynchronous code that reads like synchronous code using C++20 coroutines. Gaining traction in the Qt community.

// QCoro for async/await-style code (third-party, gaining traction)
QCoro::Task fetchData() {
    auto *reply = co_await m_network.get(request);
    auto data = reply->readAll();
    co_await processAsync(data);
    emit ready(data);
}

Anti-Patterns to Avoid

God Object

One class does everything: load, parse, render, save, network, settings — 200+ methods.

→ Split into DataManager, NetworkService, ChartRenderer.

Blocking the main thread

downloadFile(url) freezes the UI.

→ Use QNetworkAccessManager (async) or QtConcurrent::run.

String-based signal/slot

SIGNAL(valueChanged(int)) — runtime errors, no type checking.

→ Use pointer-based connect syntax.

Complex JavaScript in QML

Business logic in QML JavaScript → untestable, slow, hard to maintain.

→ Push logic to C++ backend, QML is view only.

Manual memory management for QObjects

new QDialog() without parent — who deletes it?

→ Always set a parent or use deleteLater().

Not using QStringLiteral

"hello" + " world" creates temporary QStrings.

→ Use QStringLiteral() or QLatin1StringView.

Deep inheritance hierarchies

QObject → AbstractWidget → BaseChart → LineChart → AnimatedLineChart

→ Prefer composition over inheritance. Use interfaces.

Globbing sources in CMake

file(GLOB *.cpp) — breaks when files are added without re-running CMake.

→ Explicitly list sources for reproducible builds.

Conclusion

After analyzing 200+ repositories, the picture is clear: the Qt ecosystem has standardized aroundCMake + Qt6 + QML_ELEMENT. QMake is dead for new projects. Manual qmlRegisterType is dead. The modern stack is declarative registration via CMake macros, component-basedfind_package, andqt_add_qml_module for QML.

The most important lesson: architecture choice depends on project type. For widget-based desktop apps, use Pimpl + signal/slot + parent-child hierarchy. For QML-based modern UIs, use QML_ELEMENT + qt_add_qml_module + C++ models. For complex 2D graphics editing, use QGraphicsView — QML is not a silver bullet.

On testing: qTox's modular 20-binary QtTest approach is the gold standard for C++ unit tests. Serial-Studio's pytest black-box API testing is the modern approach for integration testing. For CI, jurplel/install-qt-action@v4is the simplest entry point — Docker-based multi-distro builds are worth the complexity only for projects that ship Linux packages.

Three patterns from this research that will immediately improve any Qt codebase: (1) co-locate .cpp + .h in functional directories, (2) use QML_ELEMENT instead of manual registration, (3) add clang-format and qmlformat as CMake custom targets.

Read the full guide on GitHub

1,400+ line complete guide, 9 case studies with code excerpts, 329-repo catalog, and a reusable Python fetcher script.

github.com/shakg/qt-patterns-guide

Recommended repositories to study