diff options
14 files changed, 433 insertions, 2 deletions
diff --git a/examples/quick/models/CMakeLists.txt b/examples/quick/models/CMakeLists.txt index e8756b65ec..ca1ffc26a1 100644 --- a/examples/quick/models/CMakeLists.txt +++ b/examples/quick/models/CMakeLists.txt @@ -1,6 +1,7 @@ -# Copyright (C) 2022 The Qt Company Ltd. +# Copyright (C) 2025 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause qt_internal_add_example(abstractitemmodel) qt_internal_add_example(objectlistmodel) qt_internal_add_example(stringlistmodel) +qt_internal_add_example(threadedfetchmore) diff --git a/examples/quick/models/models.pro b/examples/quick/models/models.pro index 95d2716836..7e86f14847 100644 --- a/examples/quick/models/models.pro +++ b/examples/quick/models/models.pro @@ -2,4 +2,5 @@ TEMPLATE = subdirs SUBDIRS = \ abstractitemmodel \ objectlistmodel \ - stringlistmodel + stringlistmodel \ + threadedfetchmore diff --git a/examples/quick/models/threadedfetchmore/CMakeLists.txt b/examples/quick/models/threadedfetchmore/CMakeLists.txt new file mode 100644 index 0000000000..e53a1c8ee8 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +project(threadedfetchmore LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick) + +qt_standard_project_setup(REQUIRES 6.8) + +qt_add_executable(appthreadedfetchmore WIN32 MACOSX_BUNDLE + main.cpp +) + +qt_add_qml_module(appthreadedfetchmore + URI threadedfetchmore + QML_FILES + ContactBookDelegate.qml + ThreadedFetchMore.qml + SOURCES + fetchworker.h + fetchworker.cpp + threadedfetchmoremodel.h + threadedfetchmoremodel.cpp +) + +target_link_libraries(appthreadedfetchmore + PRIVATE Qt6::Quick +) + +include(GNUInstallDirs) +install(TARGETS appthreadedfetchmore + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_qml_app_script( + TARGET appthreadedfetchmore + OUTPUT_SCRIPT deploy_script + MACOS_BUNDLE_POST_BUILD + NO_UNSUPPORTED_PLATFORM_ERROR + DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM +) +install(SCRIPT ${deploy_script}) diff --git a/examples/quick/models/threadedfetchmore/ContactBookDelegate.qml b/examples/quick/models/threadedfetchmore/ContactBookDelegate.qml new file mode 100644 index 0000000000..b93cb4aa9c --- /dev/null +++ b/examples/quick/models/threadedfetchmore/ContactBookDelegate.qml @@ -0,0 +1,44 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + color: "white" + border.width: model.isLoadingElement ? 0 : 1 + border.color: "black" + implicitHeight: 50 + RowLayout { + anchors.fill: parent + spacing: 5 + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + implicitHeight: 30 + implicitWidth: implicitHeight + visible: model.isLoadingElement + } + Label { + font.pixelSize: 30 + font.bold: true + text: model.number + visible: !model.isLoadingElement + } + ColumnLayout { + implicitHeight: 40 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 3 + visible: !model.isLoadingElement + Label { + font.pixelSize: 16 + text: model.title + } + Label { + font.pixelSize: 14 + text: model.subtitle + } + } + } +} diff --git a/examples/quick/models/threadedfetchmore/ThreadedFetchMore.qml b/examples/quick/models/threadedfetchmore/ThreadedFetchMore.qml new file mode 100644 index 0000000000..f1c3202728 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/ThreadedFetchMore.qml @@ -0,0 +1,33 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Window { + width: 320 + height: 480 + color: "white" + visible: true + title: qsTr("Threaded fetch more example") + Rectangle { + color: "black" + width: scrollbar.width + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + } + ListView { + id: listView + anchors.fill: parent + model: ThreadedFetchMoreModel {} + delegate: ContactBookDelegate { + required property var model + width: listView.width - scrollbar.width + } + ScrollBar.vertical: ScrollBar { + id: scrollbar + policy: ScrollBar.AlwaysOn + } + } +} diff --git a/examples/quick/models/threadedfetchmore/doc/images/qml-threadedfetchmore-example.png b/examples/quick/models/threadedfetchmore/doc/images/qml-threadedfetchmore-example.png Binary files differnew file mode 100644 index 0000000000..3a4ce51b04 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/doc/images/qml-threadedfetchmore-example.png diff --git a/examples/quick/models/threadedfetchmore/doc/src/threadedfetchmore-example.qdoc b/examples/quick/models/threadedfetchmore/doc/src/threadedfetchmore-example.qdoc new file mode 100644 index 0000000000..89705a43cf --- /dev/null +++ b/examples/quick/models/threadedfetchmore/doc/src/threadedfetchmore-example.qdoc @@ -0,0 +1,39 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example models/threadedfetchmore + \title Models and Views: Fetch More functionality using a worker thread + \brief Demonstrates how to implement fetchMore() in a worker thread while maintaining a responsive UI. + \examplecategory {User Interface Components} + \image qml-threadedfetchmore-example.png + + This example shows how to utilize QAbstractItemModel::fetchMore() with an + object moved to a QThread so that the data fetching does not block the UI. + On each call, the \c FetchWorker sleeps for 2 seconds, to simulate a slow + backend service, before sending more data to the UI thread. + + \section1 Basic functionality + + While data is being fetched in the worker thread, the model adds a + BusyIndicator to the end of list. Once data is successfully fetched, the + BusyIndicator is removed, and new items are appended to the list. + The ListView is used in the typical way, and does not need adjustment + to deal with the slow model. + + \section1 Responsibilities + + The item model changes (in this case inserting and removing rows) must + happen in the UI thread. The worker thread object slowly constructs + DataBlock structs, and emits the \c dataFetched signal with a QList of data + blocks as the payload; the signal is sent via a Qt::QueuedConnection to the + ThreadedFetchMoreModel::dataReceived() slot, which appends them to the data + list in the UI thread. The UI thread adds a placeholder item to the end of + the list before sending the fetchDataBlock() signal to the worker object to + kick off the fetching process, and removes the placeholder before appending + new items to the list. + + After all available data is fetched, the worker thread object sends the + \c noMoreToFetch signal to the model; from then on, the canFetchMore() + method always returns \c false. +*/ diff --git a/examples/quick/models/threadedfetchmore/fetchworker.cpp b/examples/quick/models/threadedfetchmore/fetchworker.cpp new file mode 100644 index 0000000000..33b0272914 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/fetchworker.cpp @@ -0,0 +1,46 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "fetchworker.h" + +#include <QThread> + +static constexpr int s_fetchBlockSize{50}; +static constexpr int s_maximumListSize{s_fetchBlockSize * 10}; +static constexpr int s_sleepIterations{20}; +static constexpr std::chrono::milliseconds s_sleepInterval{100}; + +FetchWorker::FetchWorker(QObject *parent) + : QObject{parent} +{} + +void FetchWorker::fetchDataBlock() +{ + if (m_existingItemsCount < s_maximumListSize) { + QList<DataBlock> itemsToSend = slowDataConstruction(m_existingItemsCount); + m_existingItemsCount += itemsToSend.size(); + emit dataFetched(itemsToSend); + } + if (m_existingItemsCount >= s_maximumListSize) + emit noMoreToFetch(); +} + +QList<FetchWorker::DataBlock> FetchWorker::slowDataConstruction(int fromIndex) +{ + // Block for two seconds to mimic slow data source, while allowing interruption in case of application close + for (int iterations = s_sleepIterations; + iterations > 0 && !QThread::currentThread()->isInterruptionRequested(); + --iterations) { + QThread::sleep(s_sleepInterval); + } + QList<DataBlock> returnValues; + returnValues.reserve(s_fetchBlockSize); + int number = fromIndex; + for (int blocks = 0; blocks < s_fetchBlockSize; ++blocks) { + ++number; + returnValues.append({QStringLiteral("Contact %1 name").arg(number), + QStringLiteral("Contact %1 telephone").arg(number), + number}); + } + return returnValues; +} diff --git a/examples/quick/models/threadedfetchmore/fetchworker.h b/examples/quick/models/threadedfetchmore/fetchworker.h new file mode 100644 index 0000000000..6aee317516 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/fetchworker.h @@ -0,0 +1,34 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef FETCHWORKER_H +#define FETCHWORKER_H + +#include <QObject> + +class FetchWorker : public QObject +{ + Q_OBJECT +public: + struct DataBlock + { + QString title; + QString subtitle; + int number; + }; + + explicit FetchWorker(QObject *parent = nullptr); + +signals: + void dataFetched(const QList<FetchWorker::DataBlock> &items); + void noMoreToFetch(); + +public slots: + void fetchDataBlock(); + +private: + static QList<FetchWorker::DataBlock> slowDataConstruction(int fromIndex); + int m_existingItemsCount{0}; +}; + +#endif // FETCHWORKER_H diff --git a/examples/quick/models/threadedfetchmore/main.cpp b/examples/quick/models/threadedfetchmore/main.cpp new file mode 100644 index 0000000000..fcd72d6a61 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/main.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreationFailed, + &app, + []() { QCoreApplication::exit(-1); }, + Qt::QueuedConnection); + engine.loadFromModule("threadedfetchmore", "ThreadedFetchMore"); + + return app.exec(); +} diff --git a/examples/quick/models/threadedfetchmore/threadedfetchmore.pro b/examples/quick/models/threadedfetchmore/threadedfetchmore.pro new file mode 100644 index 0000000000..2390983c06 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/threadedfetchmore.pro @@ -0,0 +1,12 @@ +TARGET = threadedfetchmore +QT += qml quick + +HEADERS = fetchworker.h \ + threadedfetchmoremodel.h +SOURCES = main.cpp \ + fetchworker.cpp \ + threadedfetchmoremodel.cpp +RESOURCES += threadedfetchmore.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/quick/models/threadedfetchmore +INSTALLS += target diff --git a/examples/quick/models/threadedfetchmore/threadedfetchmore.qrc b/examples/quick/models/threadedfetchmore/threadedfetchmore.qrc new file mode 100644 index 0000000000..de2990e405 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/threadedfetchmore.qrc @@ -0,0 +1,7 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/qt/qml/threadedfetchmore"> + <file>ContactBookDelegate.qml</file> + <file>ThreadedFetchMore.qml</file> +</qresource> +</RCC> + diff --git a/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.cpp b/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.cpp new file mode 100644 index 0000000000..a7b8d44570 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.cpp @@ -0,0 +1,103 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "threadedfetchmoremodel.h" + +ThreadedFetchMoreModel::ThreadedFetchMoreModel() +{ + auto worker = new FetchWorker(); + connect(&m_workerThread, &QThread::finished, worker, &QObject::deleteLater); + worker->moveToThread(&m_workerThread); + connect(this, &ThreadedFetchMoreModel::fetchDataBlock, worker, &FetchWorker::fetchDataBlock); + connect(worker, &FetchWorker::dataFetched, this, &ThreadedFetchMoreModel::dataReceived); + connect(worker, &FetchWorker::noMoreToFetch, this, &ThreadedFetchMoreModel::noMoreToFetch); + m_workerThread.start(); +} + +ThreadedFetchMoreModel::~ThreadedFetchMoreModel() +{ + m_workerThread.requestInterruption(); + m_workerThread.quit(); + m_workerThread.wait(); +} + +int ThreadedFetchMoreModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_dataList.size(); +} + +QVariant ThreadedFetchMoreModel::data(const QModelIndex &index, int role) const +{ + const auto item = m_dataList.at(index.row()); + switch (static_cast<Role>(role)) { + case Role::Title: + return item.title; + case Role::Subtitle: + return item.subtitle; + case Role::Number: + return item.number; + case Role::LoadingElement: + return m_fetchOngoing && index.row() == (rowCount() - 1); + default: + break; + } + return QVariant{}; +} + +void ThreadedFetchMoreModel::fetchMore(const QModelIndex &parent) +{ + Q_UNUSED(parent) + if (!m_fetchOngoing) { + addLoadingItem(); + emit fetchDataBlock(); + } +} + +bool ThreadedFetchMoreModel::canFetchMore(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + if (m_fetchOngoing) + return false; + return m_hasUnfetchedItems; +} + +void ThreadedFetchMoreModel::dataReceived(const QList<FetchWorker::DataBlock> &items) +{ + removeLoadingItem(); + beginInsertRows(QModelIndex{}, rowCount(), rowCount() + items.size() - 1); + m_dataList.append(items); + endInsertRows(); +} + +void ThreadedFetchMoreModel::noMoreToFetch() +{ + m_hasUnfetchedItems = false; +} + +void ThreadedFetchMoreModel::addLoadingItem() +{ + beginInsertRows(QModelIndex{}, rowCount(), rowCount()); + m_fetchOngoing = true; + m_dataList.append({{}, {}, 0}); + endInsertRows(); +} + +void ThreadedFetchMoreModel::removeLoadingItem() +{ + beginRemoveRows(QModelIndex{}, rowCount() - 1, rowCount() - 1); + m_fetchOngoing = false; + m_dataList.removeAt(rowCount() - 1); + endRemoveRows(); +} + +QHash<int, QByteArray> ThreadedFetchMoreModel::roleNames() const +{ + static const QHash<int, QByteArray> names = {{static_cast<int>(Role::Title), "title"}, + {static_cast<int>(Role::Subtitle), "subtitle"}, + {static_cast<int>(Role::Number), "number"}, + {static_cast<int>(Role::LoadingElement), + "isLoadingElement"}}; + return names; +} diff --git a/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.h b/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.h new file mode 100644 index 0000000000..2f2c369f15 --- /dev/null +++ b/examples/quick/models/threadedfetchmore/threadedfetchmoremodel.h @@ -0,0 +1,44 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef THREADEDFETCHMOREMODEL_H +#define THREADEDFETCHMOREMODEL_H +#include <fetchworker.h> + +#include <QAbstractListModel> +#include <QThread> +#include <QtQml/qqmlregistration.h> + +class ThreadedFetchMoreModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT +public: + ThreadedFetchMoreModel(); + ~ThreadedFetchMoreModel(); + Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_INVOKABLE void fetchMore(const QModelIndex &parent) override; + Q_INVOKABLE bool canFetchMore(const QModelIndex &parent) const override; + QHash<int, QByteArray> roleNames() const override; + +signals: + void fetchDataBlock(); + +protected: + enum class Role { Title = Qt::ItemDataRole::UserRole, Subtitle, Number, LoadingElement }; + +private slots: + void dataReceived(const QList<FetchWorker::DataBlock> &items); + void noMoreToFetch(); + +private: + void addLoadingItem(); + void removeLoadingItem(); + QList<FetchWorker::DataBlock> m_dataList; + QThread m_workerThread; + bool m_fetchOngoing{false}; + bool m_hasUnfetchedItems{true}; +}; + +#endif // UNTHREADEDFETCHMOREMODEL_H |