aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/quick/models/CMakeLists.txt3
-rw-r--r--examples/quick/models/models.pro3
-rw-r--r--examples/quick/models/threadedfetchmore/CMakeLists.txt46
-rw-r--r--examples/quick/models/threadedfetchmore/ContactBookDelegate.qml44
-rw-r--r--examples/quick/models/threadedfetchmore/ThreadedFetchMore.qml33
-rw-r--r--examples/quick/models/threadedfetchmore/doc/images/qml-threadedfetchmore-example.pngbin0 -> 9522 bytes
-rw-r--r--examples/quick/models/threadedfetchmore/doc/src/threadedfetchmore-example.qdoc39
-rw-r--r--examples/quick/models/threadedfetchmore/fetchworker.cpp46
-rw-r--r--examples/quick/models/threadedfetchmore/fetchworker.h34
-rw-r--r--examples/quick/models/threadedfetchmore/main.cpp21
-rw-r--r--examples/quick/models/threadedfetchmore/threadedfetchmore.pro12
-rw-r--r--examples/quick/models/threadedfetchmore/threadedfetchmore.qrc7
-rw-r--r--examples/quick/models/threadedfetchmore/threadedfetchmoremodel.cpp103
-rw-r--r--examples/quick/models/threadedfetchmore/threadedfetchmoremodel.h44
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
new file mode 100644
index 0000000000..3a4ce51b04
--- /dev/null
+++ b/examples/quick/models/threadedfetchmore/doc/images/qml-threadedfetchmore-example.png
Binary files differ
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