--- name: qt-cpp description: Comprehensive guide for developing cross-platform desktop applications with Qt in C++, including Qt5/Qt6, CMake, signals/slots, widgets, QML, threading, and deployment. metadata: author: mte90 version: 1.0.0 tags: - qt - c++ - gui - desktop - qt6 - cmake - cross-platform - qml - threading --- # Qt C++ Development Complete reference for building cross-platform desktop applications with Qt framework in C++. ## Overview Qt is a powerful C++ framework for cross-platform application development. It provides rich GUI capabilities, networking, database access, and more. **Key Characteristics:** - Cross-platform (Windows, macOS, Linux, mobile) - Rich widget library and QML for modern UIs - Signal-slot mechanism for event handling - Meta-Object System (MOC) for reflection - Comprehensive documentation and examples ## Qt Versions ### Qt5 vs Qt6 | Feature | Qt5 | Qt6 | |---------|-----|-----| | C++ Standard | C++11 | C++17 minimum | | Graphics | OpenGL | Vulkan/Metal/DirectX | | High-DPI | Manual | Enabled by default | | QString | UTF-16 | UTF-8 by default | | Status | Maintenance | Active development | **Use Qt 6 for:** New projects, modern C++17/20 features, better graphics performance **Use Qt 5 for:** Legacy codebases, Qt 5-only modules, platform constraints ## Installation ### Qt Online Installer (Recommended) ```bash # Download from https://www.qt.io/download # Install using the Qt Online Installer # Select: Qt 6.x, Qt Creator, CMake, MinGW or MSVC ``` ### Package Managers ```bash # macOS (Homebrew) brew install qt@6 brew install cmake # Ubuntu/Debian sudo apt install qt6-base-dev qt6-tools-dev cmake # Arch Linux sudo pacman -S qt6-base cmake ``` ### Setting PATH ```bash # Add to ~/.bashrc or ~/.zshrc export PATH="/path/to/Qt/6.5.0/gcc_64/bin:$PATH" export CMAKE_PREFIX_PATH="/path/to/Qt/6.5.0/gcc_64:$CMAKE_PREFIX_PATH" ``` ## CMake Build System ### Basic CMakeLists.txt ```cmake cmake_minimum_required(VERSION 3.16) project(MyQtApp VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) set(PROJECT_SOURCES main.cpp mainwindow.cpp ) set(PROJECT_HEADERS mainwindow.h ) set(PROJECT_FORMS mainwindow.ui ) add_executable(MyQtApp ${PROJECT_SOURCES} ${PROJECT_HEADERS} ${PROJECT_FORMS} ) target_link_libraries(MyQtApp Qt6::Core Qt6::Gui Qt6::Widgets ) ``` ### Advanced CMake Configuration ```cmake cmake_minimum_required(VERSION 3.20) project(MyQtApp VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Qt configuration set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC_OPTIONS "-binary") # Find Qt6 packages find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Xml Concurrent ) # Find optional components find_package(Qt6 QUIET COMPONENTS Quick QuickControls2 Qml WebEngineWidgets ) # Source files set(PROJECT_SOURCES src/main.cpp src/mainwindow.cpp src/settingsdialog.cpp ) set(PROJECT_HEADERS src/mainwindow.h src/settingsdialog.h ) set(PROJECT_FORMS ui/mainwindow.ui ui/settingsdialog.ui ) set(PROJECT_QML qml/Main.qml ) set(PROJECT_RESOURCES resources/resources.qrc ) # Create executable add_executable(MyQtApp ${PROJECT_SOURCES} ${PROJECT_HEADERS} ${PROJECT_FORMS} ${PROJECT_QML} ${PROJECT_RESOURCES} ) # Link libraries target_link_libraries(MyQtApp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Sql Qt6::Xml Qt6::Concurrent ) # Optional: Qt Quick if(TARGET Qt6::Quick) target_link_libraries(MyQtApp PRIVATE Qt6::Quick Qt6::Qml) endif() # Installation rules install(TARGETS MyQtApp BUNDLE DESTINATION . RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) ``` ### Building ```bash mkdir build && cd build cmake .. cmake --build . ./MyQtApp ``` ## Signals and Slots ### Basic Signal-Slot Connection ```cpp // sender.h #include #include class Sender : public QObject { Q_OBJECT public: explicit Sender(QObject *parent = nullptr) : QObject(parent) { m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &Sender::timeout); m_timer->start(1000); } signals: void timeout(); void progress(int value); private: QTimer *m_timer; }; // receiver.h #include class Receiver : public QObject { Q_OBJECT public slots: void handleTimeout() { qDebug() << "Timeout received!"; } void handleProgress(int value) { qDebug() << "Progress:" << value; } }; // main.cpp int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Sender sender; Receiver receiver; // Qt5 connection syntax QObject::connect(&sender, SIGNAL(timeout()), &receiver, SLOT(handleTimeout())); // Qt6 modern connection syntax (recommended) QObject::connect(&sender, &Sender::timeout, &receiver, &Receiver::handleTimeout); return app.exec(); } ``` ### Lambda Connections ```cpp class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { QPushButton *button = new QPushButton("Click me", this); // Lambda with capture connect(button, &QPushButton::clicked, this, [this]() { handleButtonClick(); }); // Lambda with parameters connect(button, &QPushButton::clicked, this, [=](bool checked) { qDebug() << "Button clicked:" << checked; }); // Lambda with context object connect(button, &QPushButton::clicked, this, [this]() { this->handleButtonClick(); }, Qt::QueuedConnection); } private: void handleButtonClick() { qDebug() << "Button clicked!"; } }; ``` ### Connection Types ```cpp // Auto Connection (default) connect(sender, &Sender::signal, receiver, &Receiver::slot); // Uses DirectConnection if receiver and sender are in same thread // Uses QueuedConnection otherwise // Direct Connection (same thread only) connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::DirectConnection); // Slot executes immediately in sender's thread // Queued Connection (different threads) connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection); // Slot executes in receiver's thread event loop // Unique Connection (prevents duplicates) connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::UniqueConnection); // Blocking Queued Connection (blocks sender until slot finishes) connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::BlockingQueuedConnection); ``` ### Signal Forwarding ```cpp class Relay : public QObject { Q_OBJECT public: explicit Relay(QObject *parent = nullptr) : QObject(parent) {} // Relay signals void relaySignal(int value) { emit forwarded(value); } signals: void forwarded(int value); }; // Usage Source source; Relay relay; Destination dest; connect(&source, &Source::data, &relay, &Relay::relaySignal); connect(&relay, &Relay::forwarded, &dest, &Destination::handleData); ``` ## Threading ### QThread ```cpp class WorkerThread : public QThread { Q_OBJECT public: explicit WorkerThread(QObject *parent = nullptr) : QThread(parent) {} protected: void run() override { // Long-running operation for (int i = 0; i < 100; ++i) { QThread::msleep(100); emit progress(i); } } signals: void progress(int value); }; // Usage class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { QPushButton *startButton = new QPushButton("Start", this); QProgressBar *progressBar = new QProgressBar(this); m_worker = new WorkerThread(this); connect(startButton, &QPushButton::clicked, this, [this]() { m_worker->start(); }); connect(m_worker, &WorkerThread::progress, progressBar, &QProgressBar::setValue); connect(m_worker, &WorkerThread::finished, this, []() { qDebug() << "Worker finished"; }); } private: WorkerThread *m_worker; }; ``` ### QThreadPool and QRunnable ```cpp class MyTask : public QRunnable { public: explicit MyTask(int id) : m_id(id) {} void run() override { // Background task qDebug() << "Task" << m_id << "running in thread:" << QThread::currentThread(); QThread::sleep(2); } private: int m_id; }; // Usage QThreadPool pool; pool.setMaxThreadCount(4); for (int i = 0; i < 10; ++i) { pool.start(new MyTask(i)); } // Wait for all tasks pool.waitForDone(); ``` ### QtConcurrent ```cpp #include #include #include // Run function in background int heavyCalculation(int n) { QThread::sleep(2); return n * n; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // Run in separate thread QFuture future = QtConcurrent::run(heavyCalculation, 42); // Watch for completion QFutureWatcher watcher; connect(&watcher, &QFutureWatcher::finished, &app, [&future]() { qDebug() << "Result:" << future.result(); app.quit(); }); watcher.setFuture(future); return app.exec(); } // Map-Reduce pattern QVector data = {1, 2, 3, 4, 5}; // Map: apply function to all elements QFuture> mapped = QtConcurrent::mapped(data, [](int n) { return n * n; }); // Filter: keep elements that match QFuture> filtered = QtConcurrent::filtered(data, [](int n) { return n > 2; }); ``` ### moveToThread ```cpp class Worker : public QObject { Q_OBJECT public: explicit Worker(QObject *parent = nullptr) : QObject(parent) {} public slots: void doWork() { qDebug() << "Worker running in thread:" << QThread::currentThread(); QThread::sleep(2); emit workFinished("Done"); } signals: void workFinished(const QString &result); }; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { QThread *thread = new QThread(this); m_worker = new Worker(); // Move worker to thread m_worker->moveToThread(thread); // Start thread connect(thread, &QThread::started, m_worker, &Worker::doWork); connect(m_worker, &Worker::workFinished, this, [](const QString &result) { qDebug() << "Result:" << result; }); connect(m_worker, &Worker::workFinished, thread, &QThread::quit); connect(thread, &QThread::finished, thread, &QThread::deleteLater); connect(thread, &QThread::finished, m_worker, &Worker::deleteLater); thread->start(); } private: Worker *m_worker; }; ``` ## QML Integration ### Exposing C++ Objects to QML ```cpp // dataobject.h #include #include class DataObject : public QObject { Q_OBJECT Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: explicit DataObject(QObject *parent = nullptr) : QObject(parent), m_value(0) {} QString name() const { return m_name; } void setName(const QString &name) { if (m_name != name) { m_name = name; emit nameChanged(); } } int value() const { return m_value; } void setValue(int value) { if (m_value != value) { m_value = value; emit valueChanged(); } } Q_INVOKABLE void reset() { setName(""); setValue(0); } signals: void nameChanged(); void valueChanged(); private: QString m_name; int m_value; }; // main.cpp #include #include #include int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; // Register type qmlRegisterType("MyApp", 1, 0, "DataObject"); // Or expose instance DataObject *obj = new DataObject(&app); obj->setName("Test"); engine.rootContext()->setContextProperty("dataObject", obj); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); return app.exec(); } ``` ### QML File ```qml import QtQuick 2.15 import QtQuick.Controls 2.15 import MyApp 1.0 ApplicationWindow { width: 400 height: 300 visible: true title: "Qt Quick Example" Column { anchors.centerIn: parent spacing: 10 TextField { id: nameField placeholderText: "Enter name" text: dataObject.name onTextChanged: dataObject.name = text } SpinBox { value: dataObject.value onValueChanged: dataObject.value = value } Button { text: "Reset" onClicked: dataObject.reset() } Text { text: dataObject.name + " - " + dataObject.value } } } ``` ### Q_PROPERTY Attributes ```cpp class Person : public QObject { Q_OBJECT // READ: getter method // WRITE: setter method (optional) // NOTIFY: signal emitted when value changes // CONSTANT: value never changes // FINAL: property cannot be overridden Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged CONSTANT FINAL) Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) public: QString name() const { return m_name; } void setName(const QString &name) { if (m_name != name) { m_name = name; emit nameChanged(); } } int age() const { return m_age; } void setAge(int age) { if (m_age != age) { m_age = age; emit ageChanged(); } } signals: void nameChanged(); void ageChanged(); private: QString m_name; int m_age; }; ``` ### Q_INVOKABLE Methods ```cpp class Utility : public QObject { Q_OBJECT public: explicit Utility(QObject *parent = nullptr) : QObject(parent) {} // Can be called from QML Q_INVOKABLE QString reverse(const QString &text) { QString reversed; for (int i = text.length() - 1; i >= 0; --i) { reversed += text[i]; } return reversed; } Q_INVOKABLE void log(const QString &message) { qDebug() << "QML:" << message; } }; ``` ## Model/View Programming ### QAbstractListModel ```cpp #include #include class TodoModel : public QAbstractListModel { Q_OBJECT public: enum Roles { TextRole = Qt::UserRole + 1, DoneRole }; explicit TodoModel(QObject *parent = nullptr) : QAbstractListModel(parent) {} int rowCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent); return m_todos.size(); } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || index.row() >= m_todos.size()) return QVariant(); const Todo &todo = m_todos[index.row()]; switch (role) { case TextRole: return todo.text; case DoneRole: return todo.done; default: return QVariant(); } } QHash roleNames() const override { QHash roles; roles[TextRole] = "text"; roles[DoneRole] = "done"; return roles; } Q_INVOKABLE void add(const QString &text) { beginInsertRows(QModelIndex(), m_todos.size(), m_todos.size()); m_todos.append({text, false}); endInsertRows(); } Q_INVOKABLE void remove(int index) { if (index < 0 || index >= m_todos.size()) return; beginRemoveRows(QModelIndex(), index, index); m_todos.removeAt(index); endRemoveRows(); } Q_INVOKABLE void toggle(int index) { if (index < 0 || index >= m_todos.size()) return; m_todos[index].done = !m_todos[index].done; emit dataChanged(createIndex(index, 0), createIndex(index, 0), {DoneRole}); } private: struct Todo { QString text; bool done; }; QVector m_todos; }; ``` ### QML ListView with Model ```qml import QtQuick 2.15 import QtQuick.Controls 2.15 ListView { width: 400 height: 500 model: todoModel delegate: ItemDelegate { width: ListView.view.width height: 50 CheckBox { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter checked: model.done onClicked: todoModel.toggle(index) } Text { anchors.left: checkbox.right anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: model.text elide: Text.ElideRight } } Button { anchors.bottom: parent.bottom anchors.right: parent.right text: "Add" onClicked: todoModel.add("New todo") } } ``` ## Qt 6 Specific Features ### QString UTF-8 by Default ```cpp // Qt5: UTF-16 QString text = "Hello"; // Internally UTF-16 QByteArray bytes = text.toUtf8(); // Need explicit conversion // Qt6: UTF-8 by default QString text = "Hello"; // Internally UTF-8 QByteArray bytes = text.toLatin1(); // Need explicit conversion for Latin-1 ``` ### New Graphics Stack ```cpp // Qt6 uses RHI (Rendering Hardware Interface) // Supports Vulkan, Metal, Direct3D 11/12, OpenGL // For Qt Quick 3D import QtQuick3D 6.0 Model { id: cube source: "#Cube" scale: Qt.vector3d(2, 2, 2) } Node { Model { id: sceneModel source: "#Rectangle" scale: Qt.vector3d(10, 10, 1) materials: PrincipledMaterial { baseColor: "green" } } } ``` ### Properties ```cpp // Qt6 properties (similar to Q_PROPERTY but simpler) class Counter : public QObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged FINAL) QML_ELEMENT // Register for QML public: int value() const { return m_value; } void setValue(int value) { if (m_value != value) { m_value = value; emit valueChanged(); } } // Property binding (Qt6 feature) Q_PROPERTY(int doubled READ doubled NOTIFY valueChanged FINAL) int doubled() const { return m_value * 2; } signals: void valueChanged(); private: int m_value = 0; }; ``` ## Deployment ### Windows (windeployqt) ```bash # Build release mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release # Deploy windeployqt.exe --release MyQtApp.exe # With plugins windeployqt.exe --release --no-translations MyQtApp.exe # With OpenSSL (if using QtNetwork with SSL) windeployqt.exe --release --no-translations MyQtApp.exe xcopy /E /I /Y C:\OpenSSL\*.dll deploy ``` ### macOS (macdeployqt) ```bash # Build release mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release # Deploy macdeployqt MyQtApp.app -dmg # With frameworks macdeployqt MyQtApp.app -verbose=3 # Code sign (requires developer certificate) codesign --deep --force --verify --verbose \ MyQtApp.app # Create DMG hdiutil create -volname "MyQtApp" -srcfolder MyQtApp.app \ -ov -format UDZO MyQtApp.dmg ``` ### Linux (linuxdeployqt) ```bash # Install linuxdeployqt wget https://github.com/linuxdeploy/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage chmod +x linuxdeployqt-continuous-x86_64.AppImage # Build release mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release # Deploy ./linuxdeployqt-continuous-x86_64.AppImage MyQtApp # Create AppImage ./linuxdeployqt-continuous-x86_64.AppImage \ --appdir AppDir \ --output appimage ``` ### CMake Installation Rules ```cmake # Platform-specific installation if(WIN32) install(TARGETS MyQtApp RUNTIME DESTINATION . ) # Deploy with windeployqt install(CODE "${CMAKE_COMMAND}" -E env "PATH=$ENV{PATH}" "${CMAKE_PREFIX_PATH}/bin/windeployqt.exe" --dir "${CMAKE_INSTALL_PREFIX}" --no-translations MyQtApp.exe ) elseif(APPLE) install(TARGETS MyQtApp BUNDLE DESTINATION . ) # Deploy with macdeployqt install(CODE execute_process(COMMAND ${CMAKE_PREFIX_PATH}/bin/macdeployqt "${CMAKE_INSTALL_PREFIX}/MyQtApp.app" -dmg) ) else() install(TARGETS MyQtApp RUNTIME DESTINATION bin ) # Desktop file install(FILES MyQtApp.desktop DESTINATION share/applications) # Icons install(FILES icons/myqtapp.png DESTINATION share/icons/hicolor/256x256/apps) endif() ``` ## Common Issues and Solutions ### Memory Leaks **Problem:** QObject children not deleted correctly ```cpp // ❌ BAD: Manual deletion can cause issues delete myWidget; // May crash if parent still exists // ✅ GOOD: Use deleteLater() or parent-child system myWidget->deleteLater(); // Or myWidget->setParent(nullptr); delete myWidget; ``` ### Thread Safety **Problem:** GUI updates from wrong thread ```cpp // ❌ BAD: Updating UI from worker thread class Worker : public QObject { void run() { label->setText("Done"); // CRASH! } }; // ✅ GOOD: Use signals to update UI class Worker : public QObject { void run() { emit updateText("Done"); // Safe } signals: void updateText(const QString &text); }; connect(worker, &Worker::updateText, label, &QLabel::setText, Qt::QueuedConnection); ``` ### Signal-Slot Connection Issues **Problem:** Signals not connected ```cpp // ❌ BAD: Using old syntax in Qt6 connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(handleValue(int))); // ✅ GOOD: Use modern syntax connect(sender, &Sender::valueChanged, receiver, &Receiver::handleValue); // ✅ GOOD: Runtime check if (!connect(sender, &Sender::valueChanged, receiver, &Receiver::handleValue)) { qWarning() << "Failed to connect signal"; } ``` ### QML Binding Issues **Problem:** Properties not updating in QML ```cpp // ❌ BAD: Forgetting NOTIFY class Counter : public QObject { Q_PROPERTY(int value READ value WRITE setValue) // Missing NOTIFY }; // ✅ GOOD: Always include NOTIFY for writable properties class Counter : public QObject { Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) void setValue(int value) { if (m_value != value) { m_value = value; emit valueChanged(); // Must emit! } } signals: void valueChanged(); }; ``` ### Build Errors **Problem:** MOC not running ```cmake # ❌ BAD: Missing AUTOMOC add_executable(MyQtApp main.cpp mainwindow.cpp) target_link_libraries(MyQtApp Qt6::Widgets) # ✅ GOOD: Enable AUTOMOC set(CMAKE_AUTOMOC ON) add_executable(MyQtApp main.cpp mainwindow.cpp) target_link_libraries(MyQtApp Qt6::Widgets) ``` ### Deployment Issues **Problem:** Missing plugins on target system ```bash # Windows: Missing DLLs windeployqt.exe MyQtApp.exe --verbose # macOS: Missing frameworks macdeployqt MyQtApp.app --verbose=3 # Linux: Missing libraries ldd ./MyQtApp # Check dependencies ``` ## Best Practices 1. **Always use Qt6 for new projects** 2. **Prefer modern signal-slot syntax** (`&Sender::signal`) 3. **Use `deleteLater()` instead of `delete`** for QObjects 4. **Enable `AUTOMOC`, `AUTOUIC`, `AUTORCC`** in CMake 5. **Use Q_PROPERTY for properties exposed to QML** 6. **Avoid blocking operations in main thread** 7. **Use `moveToThread()` for worker objects** 8. **Test on all target platforms early** 9. **Use Qt Creator's visual editors** for UI design 10. **Leverage Qt's extensive documentation and examples** ## Resources - **Official Documentation:** https://doc.qt.io/qt-6/ - **Qt Wiki:** https://wiki.qt.io/ - **Qt Forum:** https://forum.qt.io/ - **Examples:** https://doc.qt.io/qt-6/examples-and-tutorials.html - **Qt Creator:** https://www.qt.io/product/development-tools - **Qt Project Hosting:** https://codereview.qt-project.org/ ## Quick Reference ### Common Headers ```cpp #include // GUI application #include // Main window #include // Basic widget #include // Button #include // Text label #include // Text input #include // Layout #include // Timer #include // Threading #include // Network #include // Database ``` ### Common CMake Components ```cmake find_package(Qt6 REQUIRED COMPONENTS Core # Core non-GUI functionality Gui # Window system, events Widgets # UI widgets Network # Network programming Sql # SQL database Xml # XML/SAX/DOM parsers Concurrent # Threading utilities ) ``` ### Common Qt6 Modules - **Qt6::Core** - Core functionality - **Qt6::Gui** - Windowing, events, 2D graphics - **Qt6::Widgets** - UI widgets - **Qt6::Quick** - QML framework - **Qt6::Network** - Network APIs - **Qt6::Sql** - SQL database - **Qt6::Xml** - XML processing - **Qt6::Test** - Unit testing framework - **Qt6::Concurrent** - Threading utilities