aboutsummaryrefslogtreecommitdiffstats
path: root/examples/quick
diff options
context:
space:
mode:
Diffstat (limited to 'examples/quick')
-rw-r--r--examples/quick/models/CMakeLists.txt1
-rw-r--r--examples/quick/models/models.pro3
-rw-r--r--examples/quick/models/threadedsonglist/CMakeLists.txt80
-rw-r--r--examples/quick/models/threadedsonglist/SongListDelegate.qml93
-rw-r--r--examples/quick/models/threadedsonglist/ThreadedSongList.qml151
-rw-r--r--examples/quick/models/threadedsonglist/datastorage.cpp84
-rw-r--r--examples/quick/models/threadedsonglist/datastorage.h49
-rw-r--r--examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webpbin0 -> 40194 bytes
-rw-r--r--examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc96
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical1.jpegbin0 -> 2101 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical2.jpegbin0 -> 2464 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical3.jpegbin0 -> 1724 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical4.jpegbin0 -> 1953 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical5.jpegbin0 -> 1730 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/classical6.jpegbin0 -> 2193 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal1.jpegbin0 -> 2119 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal2.jpegbin0 -> 2316 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal3.jpegbin0 -> 1942 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal4.jpegbin0 -> 2101 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal5.jpegbin0 -> 1598 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/metal6.jpegbin0 -> 1756 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop1.jpegbin0 -> 3164 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop2.jpegbin0 -> 3337 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop3.jpegbin0 -> 3551 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop4.jpegbin0 -> 3018 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop5.jpegbin0 -> 3310 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/pop6.jpegbin0 -> 3211 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock1.jpegbin0 -> 2170 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock2.jpegbin0 -> 2298 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock3.jpegbin0 -> 2443 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock4.jpegbin0 -> 2337 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock5.jpegbin0 -> 2077 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/album_covers/rock6.jpegbin0 -> 2690 bytes
-rw-r--r--examples/quick/models/threadedsonglist/images/device/remote.jpegbin0 -> 46469 bytes
-rw-r--r--examples/quick/models/threadedsonglist/main.cpp21
-rw-r--r--examples/quick/models/threadedsonglist/mediaelement.cpp42
-rw-r--r--examples/quick/models/threadedsonglist/mediaelement.h31
-rw-r--r--examples/quick/models/threadedsonglist/queueworker.cpp68
-rw-r--r--examples/quick/models/threadedsonglist/queueworker.h41
-rw-r--r--examples/quick/models/threadedsonglist/remotemedia.cpp29
-rw-r--r--examples/quick/models/threadedsonglist/remotemedia.h25
-rw-r--r--examples/quick/models/threadedsonglist/songdatagenerator.cpp182
-rw-r--r--examples/quick/models/threadedsonglist/songdatagenerator.h30
-rw-r--r--examples/quick/models/threadedsonglist/threadedlistmodel.cpp75
-rw-r--r--examples/quick/models/threadedsonglist/threadedlistmodel.h41
-rw-r--r--examples/quick/models/threadedsonglist/threadedsonglist.pro20
-rw-r--r--examples/quick/models/threadedsonglist/threadedsonglist.qrc32
47 files changed, 1193 insertions, 1 deletions
diff --git a/examples/quick/models/CMakeLists.txt b/examples/quick/models/CMakeLists.txt
index ca1ffc26a1..c874f21180 100644
--- a/examples/quick/models/CMakeLists.txt
+++ b/examples/quick/models/CMakeLists.txt
@@ -5,3 +5,4 @@ qt_internal_add_example(abstractitemmodel)
qt_internal_add_example(objectlistmodel)
qt_internal_add_example(stringlistmodel)
qt_internal_add_example(threadedfetchmore)
+qt_internal_add_example(threadedsonglist)
diff --git a/examples/quick/models/models.pro b/examples/quick/models/models.pro
index 7e86f14847..7675c4588f 100644
--- a/examples/quick/models/models.pro
+++ b/examples/quick/models/models.pro
@@ -3,4 +3,5 @@ SUBDIRS = \
abstractitemmodel \
objectlistmodel \
stringlistmodel \
- threadedfetchmore
+ threadedfetchmore \
+ threadedsonglist
diff --git a/examples/quick/models/threadedsonglist/CMakeLists.txt b/examples/quick/models/threadedsonglist/CMakeLists.txt
new file mode 100644
index 0000000000..1da9d32deb
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/CMakeLists.txt
@@ -0,0 +1,80 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.16)
+
+project(threadedsonglist VERSION 0.1 LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Quick)
+
+qt_standard_project_setup(REQUIRES 6.8)
+
+qt_add_executable(appthreadedsonglist WIN32 MACOSX_BUNDLE
+ main.cpp
+)
+
+qt_add_qml_module(appthreadedsonglist
+ URI threadedsonglist
+ QML_FILES
+ SongListDelegate.qml
+ ThreadedSongList.qml
+ SOURCES
+ datastorage.h
+ datastorage.cpp
+ mediaelement.h
+ mediaelement.cpp
+ queueworker.h
+ queueworker.cpp
+ remotemedia.h
+ remotemedia.cpp
+ songdatagenerator.h
+ songdatagenerator.cpp
+ threadedlistmodel.h
+ threadedlistmodel.cpp
+ RESOURCES
+ images/album_covers/classical1.jpeg
+ images/album_covers/classical2.jpeg
+ images/album_covers/classical3.jpeg
+ images/album_covers/classical4.jpeg
+ images/album_covers/classical5.jpeg
+ images/album_covers/classical6.jpeg
+ images/album_covers/metal1.jpeg
+ images/album_covers/metal2.jpeg
+ images/album_covers/metal3.jpeg
+ images/album_covers/metal4.jpeg
+ images/album_covers/metal5.jpeg
+ images/album_covers/metal6.jpeg
+ images/album_covers/pop1.jpeg
+ images/album_covers/pop2.jpeg
+ images/album_covers/pop3.jpeg
+ images/album_covers/pop4.jpeg
+ images/album_covers/pop5.jpeg
+ images/album_covers/pop6.jpeg
+ images/album_covers/rock1.jpeg
+ images/album_covers/rock2.jpeg
+ images/album_covers/rock3.jpeg
+ images/album_covers/rock4.jpeg
+ images/album_covers/rock5.jpeg
+ images/album_covers/rock6.jpeg
+ images/device/remote.jpeg
+)
+
+target_link_libraries(appthreadedsonglist
+ PRIVATE Qt6::Quick
+)
+
+include(GNUInstallDirs)
+install(TARGETS appthreadedsonglist
+ BUNDLE DESTINATION .
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+)
+
+qt_generate_deploy_qml_app_script(
+ TARGET appthreadedsonglist
+ 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/threadedsonglist/SongListDelegate.qml b/examples/quick/models/threadedsonglist/SongListDelegate.qml
new file mode 100644
index 0000000000..ba96d3c3d5
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/SongListDelegate.qml
@@ -0,0 +1,93 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls.Universal
+
+MouseArea {
+ id: delegateRoot
+ signal songSelected(albumName: string, albumArt: string, artistName: string, songTitle: string)
+
+ implicitHeight: 60
+ enabled: model.albumArt !== ""
+ onClicked: songSelected(model.album, model.albumArt, model.artist, model.song)
+ BusyIndicator {
+ id: loadingIndicator
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: loadingText.left
+ anchors.rightMargin: 5
+ height: loadingText.implicitHeight
+ width: height
+ running: model.loadingText === "" && model.isLoadingElement
+ }
+ Text {
+ id: loadingText
+ anchors.centerIn: parent
+ font.pixelSize: delegateRoot.implicitHeight / 4
+ text: model.loadingText === "" ? qsTr("Loading...") : model.loadingText
+ visible: model.isLoadingElement
+ }
+ Item {
+ anchors.fill: parent
+ visible: !loadingIndicator.running
+ Image {
+ id: albumArtImage
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: 1
+ anchors.topMargin: 1
+ anchors.bottomMargin: 1
+ source: model.albumArt
+ width: height
+ fillMode: Image.PreserveAspectCrop
+ }
+ Text {
+ id: artistName
+ anchors.left: albumArtImage.right
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.topMargin: 1
+ anchors.leftMargin: 1
+ anchors.rightMargin: 1
+ font.pixelSize: delegateRoot.implicitHeight / 5
+ text: model.artist
+ }
+ Text {
+ id: songName
+ anchors.left: albumArtImage.right
+ anchors.right: parent.right
+ anchors.top: artistName.bottom
+ anchors.topMargin: 1
+ anchors.leftMargin: 1
+ anchors.rightMargin: 1
+ font.pixelSize: delegateRoot.implicitHeight / 4
+ text: model.song
+ }
+ Text {
+ id: albumName
+ anchors.left: albumArtImage.right
+ anchors.right: parent.right
+ anchors.top: songName.bottom
+ anchors.topMargin: 1
+ anchors.leftMargin: 1
+ anchors.rightMargin: 1
+ font.pixelSize: delegateRoot.implicitHeight / 5
+ text: model.album
+ }
+ }
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ implicitHeight: 1
+ color: "lightgray"
+ }
+ Text {
+ anchors.right: parent.right
+ anchors.rightMargin: delegateRoot.implicitHeight / 2
+ anchors.verticalCenter: parent.verticalCenter
+ font.pixelSize: delegateRoot.implicitHeight / 4
+ text: "#"+(index+1)
+ }
+}
diff --git a/examples/quick/models/threadedsonglist/ThreadedSongList.qml b/examples/quick/models/threadedsonglist/ThreadedSongList.qml
new file mode 100644
index 0000000000..9563b64639
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/ThreadedSongList.qml
@@ -0,0 +1,151 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+
+//! [Fixed Controls style]
+import QtQuick
+import QtQuick.Controls.Universal
+//! [Fixed Controls style]
+
+Window {
+ id: mainWindow
+ minimumWidth: 800
+ minimumHeight: 480
+ width: minimumWidth
+ height: minimumHeight
+ visible: true
+ title: qsTr("Threaded Song List")
+
+ property string currentAlbumName: ""
+ property string currentAlbumArt: ""
+ property string currentArtistName: ""
+ property string currentSongTitle: ""
+ ListView {
+ id: songListView
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ width: 2 * parent.width / 5
+ model: ThreadedListModel{}
+ readonly property int delegateHeight: (songListView.height / 8) - songListView.spacing
+ cacheBuffer: delegateHeight * 2
+ delegate: SongListDelegate{
+ required property int index
+ required property var model
+ implicitHeight: songListView.delegateHeight
+ width: songListView.width
+ onSongSelected: (albumName, albumArt, artistName, songTitle) => {
+ mainWindow.currentAlbumName = albumName
+ mainWindow.currentAlbumArt = albumArt
+ mainWindow.currentArtistName = artistName
+ mainWindow.currentSongTitle = songTitle
+ }
+ }
+ ScrollBar.vertical: ScrollBar {
+ policy: ScrollBar.AlwaysOn
+ implicitWidth: songListView.height / 20
+ }
+ BusyIndicator {
+ anchors.centerIn: parent
+ width: parent.width / 3
+ height: width
+ running: songListView.count === 0
+ }
+ }
+ Rectangle {
+ color: "white"
+ anchors.left: songListView.right
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ Label {
+ id: statusLabel
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.leftMargin: 10
+ anchors.topMargin: 5
+ textFormat: Text.RichText
+ text: qsTr("🔗 Connected, %1 songs available").arg(songListView.count)
+ font.pixelSize: Math.min(parent.height, parent.width) / 32
+ horizontalAlignment: Text.AlignHCenter
+ }
+ Item {
+ id: deviceImage
+ anchors.right: parent.right
+ anchors.top: statusLabel.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: 10
+ height: (3 * parent.height / 4) - statusLabel.height
+ Image {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.min(parent.width, parent.height)
+ height: width
+ source: "qrc:/qt/qml/threadedsonglist/images/device/remote.jpeg"
+ fillMode: Image.PreserveAspectFit
+ }
+ }
+ Rectangle {
+ visible: mainWindow.currentSongTitle !== ""
+ color: "black"
+ anchors.top: deviceImage.bottom
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ Rectangle {
+ id: nowPlayingBox
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.left: parent.left
+ height: nowPlayingTitle.implicitHeight
+ color: "#555555"
+ Label {
+ id: nowPlayingTitle
+ anchors.centerIn: parent
+ color: "white"
+ text: qsTr("Now playing:")
+ font.pixelSize: 16
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ Image {
+ id: albumArtImage
+ anchors.top: nowPlayingBox.bottom
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 5
+ source: mainWindow.currentAlbumArt
+ fillMode: Image.PreserveAspectFit
+ }
+ Column {
+ anchors.top: nowPlayingBox.bottom
+ anchors.right: parent.right
+ anchors.left: albumArtImage.right
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 5
+ anchors.leftMargin: 5
+ spacing: 5
+ Label {
+ color: "white"
+ text: "♫: " + mainWindow.currentSongTitle
+ textFormat: Text.RichText
+ font.pixelSize: (parent.height - 20) / 4
+ }
+ Label {
+ color: "white"
+ text: "👤: " + mainWindow.currentArtistName
+ textFormat: Text.RichText
+ font.pixelSize: (parent.height - 20) / 4
+ }
+ Label {
+ color: "white"
+ text: "💿: " + mainWindow.currentAlbumName
+ textFormat: Text.RichText
+ font.pixelSize: (parent.height - 20) / 4
+ }
+ }
+ }
+ }
+}
diff --git a/examples/quick/models/threadedsonglist/datastorage.cpp b/examples/quick/models/threadedsonglist/datastorage.cpp
new file mode 100644
index 0000000000..c38458960d
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/datastorage.cpp
@@ -0,0 +1,84 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "datastorage.h"
+#include "queueworker.h"
+
+DataStorage::DataStorage() : QObject{ nullptr }
+{
+ /* Generate a list of IDs to use locally.
+ * DataStorage will use indexes for accessing the remote data.
+ */
+ m_idList.reserve(RemoteMedia::count());
+ for (int id = 1; id <= RemoteMedia::count(); ++id) {
+ m_idList.append(id);
+ }
+ m_worker = new QueueWorker;
+ connect(&m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater);
+ m_worker->moveToThread(&m_workerThread);
+ connect(m_worker, &QueueWorker::dataFetched, this, &DataStorage::dataReceived);
+ connect(m_worker, &QueueWorker::processing, this, &DataStorage::fetchStarted);
+ connect(m_worker, &QueueWorker::dropped, this, &DataStorage::fetchAborted);
+ connect(this, &DataStorage::dataFetchNeeded, m_worker, &QueueWorker::fetchData);
+ m_workerThread.start();
+}
+
+DataStorage::~DataStorage()
+{
+ /* Clean the worker object by stopping it and then ending the thread */
+ m_worker->abort();
+ m_worker = nullptr;
+ m_workerThread.quit();
+ m_workerThread.wait();
+}
+
+QList<int> DataStorage::idList()
+{
+ return m_idList;
+}
+
+MediaElement DataStorage::item(int id) const
+{
+ if (id < 1)
+ return MediaElement{};
+ //! [Send signal if no data]
+ if (!m_items.contains(id)) {
+ m_items.insert(id, MediaElement{});
+ emit dataFetchNeeded(m_idList.indexOf(id));
+ }
+ return m_items.value(id);
+ //! [Send signal if no data]
+}
+
+std::optional<int> DataStorage::currentlyFetchedId() const
+{
+ return m_currentlyFetchedId;
+}
+
+void DataStorage::fetchStarted(int index)
+{
+ const auto idBeingFetched = m_idList.at(index);
+ m_currentlyFetchedId = idBeingFetched;
+ emit dataUpdated(idBeingFetched);
+}
+
+void DataStorage::fetchAborted(int index)
+{
+ /* The data will never be fetched. Remove the empty item (so storge will send a signal to thread
+ * if needed). Then send dataUpdated signal. If the item is still visible, view will re-request
+ * its data and item will be requeued to thread. If item is no longer in visible area, view will
+ * ignore the dataChanged signal and storage will not receive any calls.
+ */
+ const auto idAborted = m_idList.at(index);
+ m_items.remove(idAborted);
+ if (m_currentlyFetchedId == idAborted)
+ m_currentlyFetchedId = std::nullopt;
+ emit dataUpdated(idAborted);
+}
+
+void DataStorage::dataReceived(int index, MediaElement element)
+{
+ const auto idUpdated = m_idList.at(index);
+ m_items.insert(idUpdated, element);
+ emit dataUpdated(idUpdated);
+}
diff --git a/examples/quick/models/threadedsonglist/datastorage.h b/examples/quick/models/threadedsonglist/datastorage.h
new file mode 100644
index 0000000000..d8cd4b9f2a
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/datastorage.h
@@ -0,0 +1,49 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef DATASTORAGE_H
+#define DATASTORAGE_H
+
+#include "mediaelement.h"
+
+#include <QHash>
+#include <QThread>
+
+class QueueWorker;
+
+class DataStorage : public QObject
+{
+ Q_OBJECT
+public:
+ explicit DataStorage();
+ ~DataStorage();
+
+ //! [Interface towards the model]
+ QList<int> idList();
+ MediaElement item(int id) const;
+ std::optional<int> currentlyFetchedId() const;
+ //! [Interface towards the model]
+
+signals:
+ // Towards client
+ void dataUpdated(int id);
+ // Towards worker
+ void dataFetchNeeded(int index) const;
+
+private slots:
+ // From worker
+ void fetchStarted(int index);
+ void fetchAborted(int index);
+ void dataReceived(int index, MediaElement element);
+
+private:
+ QueueWorker *m_worker;
+ QList<int> m_idList;
+ QThread m_workerThread;
+ std::optional<int> m_currentlyFetchedId;
+ /* The item list is mutable as DataStorage needs to mark already signaled items to it in
+ * const data() call of model */
+ mutable QHash<int, MediaElement> m_items;
+};
+
+#endif // DATASTORAGE_H
diff --git a/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp b/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp
new file mode 100644
index 0000000000..b0cd68b44c
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc b/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc
new file mode 100644
index 0000000000..b3c23602e7
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc
@@ -0,0 +1,96 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \example models/threadedsonglist
+ \title Models and Views: List Model using a worker thread for data fetching
+ \brief Demonstrates how to implement a list model with a responsive UI
+ using a worker thread to fetch data.
+ \examplecategory {User Interface Components}
+ \examplecategory {Data Processing & I/O}
+ \meta {tag} {quick,threads}
+ \image qml-threadedsonglist-example.webp {Screenshot of the application
+ where a songlist with album arts, song names, artist names and album names
+ are visible}
+
+ This example introduces a custom item model, inheriting QAbstractListModel.
+ The model gets its data from a worker object that is in separate QThread,
+ fetching data from a slow data source.
+
+ \section1 Overview of the Threaded Song List example
+
+ The data source simulates a slow data source by adding a delay of 100
+ milliseconds per song fetched from it. This means that loading of the
+ entire list of 3600 songs would take 6 minutes, making the opening of the
+ application impractical. This delay is mitigated by fetching the data only
+ for the visible area of the view, using a QObject placed into a worker
+ thread.
+
+ The worker object has a limit for the number of fetch requests it holds in
+ queue. This ensures that only the elements of the currently visible part of
+ the song list are fetched, removing the need to wait for the non-visible
+ part of the list to load, when user has already scrolled past some part of
+ the list.
+
+ Focus of this example is in the source model of the view. The view itself
+ is an unmodified QML ListView with a simple delegate. The use of thread is
+ hidden behind the implementation of model data handling and the ListView
+ does not need to have any customization to be able to adapt to the
+ thread-based model.
+
+ Also since the focus is in the model, the Qt Quick Controls is set to use
+ Universal style on all platforms to ensure identical UI behavior.
+
+ \snippet models/threadedsonglist/ThreadedSongList.qml Fixed Controls style
+
+ \section1 How it works
+
+ The business logic of providing the song list data is separated into
+ \c DataStorage class that provides a simple ID-based interface for the model.
+
+ \snippet models/threadedsonglist/datastorage.h Interface towards the model
+
+ When model requests data from the DataStorage, the storage will first check
+ if it has the data already available. If it does, data is returned
+ instantly, as would be the case in a non-threaded model. In case the data
+ is not found, DataStorage will emit a \c dataFetchNeeded() signal to the
+ worker object and add an empty item to the list of already existing data.
+ Adding the empty item ensures that no further signals are sent to the
+ worker for the same list item.
+
+ \snippet models/threadedsonglist/datastorage.cpp Send signal if no data
+
+ \c QueueWorker - the worker thread object - processes the dataFetchNeeded()
+ signals it has received by sending a signal to itself, which makes it
+ possible to receive all signals already in QEventQueue before starting the
+ slow data read operation.
+
+ \section1 Applying the approach to dynamic models
+
+ If one wishes to expand the solution towards a case where items may be
+ added, moved or removed from the data source (in this case RemoteMedia),
+ DataStorage needs to be updated with signals to match
+ QAbstractItemModel::rowsMoved(), QAbstractItemModel::rowsInserted() and
+ two signals to trigger the QAbstractItemModel::beginRemoveRows() and
+ QAbstractItemModel::endRemoveRows() inside ThreadedListModel.
+
+ For the insertion and move the ThreadedListModel can just call
+ QAbstractItemModel::beginInsertRows(), then add new IDs to its ID list and
+ call QAbstractItemModel::endInsertRows(). As ThreadedListModel holds a copy
+ of the ID list and accesses storage by ID, there is no need to signal the
+ begin and end from storage. Equally ThreadedListModel can call
+ QAbstractItemModel::beginMoveRows(), move IDs in its ID list and then call
+ QAbstractItemModel::endMoveRows().
+
+ Removal is a slightly more complex case. The view needs to have a
+ possibility to request the data that is going to be removed before it is
+ actually removed. So DataStorage needs to signal a warning of the removal,
+ causing the Model to call QAbstractItemModel::beginRemoveRows(). At this
+ stage ThreadedListModel may get one or more \c data() calls. Once the call
+ to direct connected signal returns at DataStorage, it is OK for DataStorage
+ to remove the item and then signal the model again with another signal that
+ triggers the model to call QAbstractItemModel::endRemoveRows().
+
+ \include examples-run.qdocinc
+
+*/
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg
new file mode 100644
index 0000000000..b7af47e53b
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg
new file mode 100644
index 0000000000..d06411375e
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg
new file mode 100644
index 0000000000..6e16f84ba9
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg
new file mode 100644
index 0000000000..07e6d72c02
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg
new file mode 100644
index 0000000000..a7e7dce536
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg
new file mode 100644
index 0000000000..9b6c146d98
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg
new file mode 100644
index 0000000000..edea49d108
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg
new file mode 100644
index 0000000000..dac8e02ece
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg
new file mode 100644
index 0000000000..c3044af90a
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg
new file mode 100644
index 0000000000..bda98ee77e
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg
new file mode 100644
index 0000000000..c467f53a19
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg
new file mode 100644
index 0000000000..0b90d744d2
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg
new file mode 100644
index 0000000000..65c09a576c
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg
new file mode 100644
index 0000000000..0bd2b28ca9
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg
new file mode 100644
index 0000000000..683dc2931b
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg
new file mode 100644
index 0000000000..72ca07f3d3
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg
new file mode 100644
index 0000000000..d7abcb0cac
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg
new file mode 100644
index 0000000000..217816a694
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg
new file mode 100644
index 0000000000..728353ce83
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg
new file mode 100644
index 0000000000..0fc626173d
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg
new file mode 100644
index 0000000000..96f3c342e7
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg
new file mode 100644
index 0000000000..4e35aa6124
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg
new file mode 100644
index 0000000000..7d3456419a
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg
new file mode 100644
index 0000000000..fb71999e14
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/images/device/remote.jpeg b/examples/quick/models/threadedsonglist/images/device/remote.jpeg
new file mode 100644
index 0000000000..9751188536
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/images/device/remote.jpeg
Binary files differ
diff --git a/examples/quick/models/threadedsonglist/main.cpp b/examples/quick/models/threadedsonglist/main.cpp
new file mode 100644
index 0000000000..60c12ad601
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/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("threadedsonglist", "ThreadedSongList");
+
+ return app.exec();
+}
diff --git a/examples/quick/models/threadedsonglist/mediaelement.cpp b/examples/quick/models/threadedsonglist/mediaelement.cpp
new file mode 100644
index 0000000000..6d93cd55ea
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/mediaelement.cpp
@@ -0,0 +1,42 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "mediaelement.h"
+
+MediaElement::MediaElement() {}
+
+MediaElement::MediaElement(QStringView songName,
+ QStringView artistName,
+ QStringView albumName,
+ const QUrl& albumArt)
+ : m_isValid{true}
+ , m_song{songName}
+ , m_artist{artistName}
+ , m_album{albumName}
+ , m_albumArtUrl{albumArt}
+{}
+
+bool MediaElement::isValid() const
+{
+ return m_isValid;
+}
+
+QUrl MediaElement::albumArtFile() const
+{
+ return m_albumArtUrl;
+}
+
+QString MediaElement::song() const
+{
+ return m_song;
+}
+
+QString MediaElement::artist() const
+{
+ return m_artist;
+}
+
+QString MediaElement::album() const
+{
+ return m_album;
+}
diff --git a/examples/quick/models/threadedsonglist/mediaelement.h b/examples/quick/models/threadedsonglist/mediaelement.h
new file mode 100644
index 0000000000..d93942f65d
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/mediaelement.h
@@ -0,0 +1,31 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef MEDIAELEMENT_H
+#define MEDIAELEMENT_H
+
+#include <QUrl>
+
+class MediaElement
+{
+public:
+ explicit MediaElement();
+ MediaElement(QStringView songName,
+ QStringView artistName,
+ QStringView albumName,
+ const QUrl &albumArt);
+ bool isValid() const;
+ QUrl albumArtFile() const;
+ QString song() const;
+ QString artist() const;
+ QString album() const;
+
+private:
+ bool m_isValid{false};
+ QString m_song;
+ QString m_artist;
+ QString m_album;
+ QUrl m_albumArtUrl;
+};
+
+#endif // MEDIAELEMENT_H
diff --git a/examples/quick/models/threadedsonglist/queueworker.cpp b/examples/quick/models/threadedsonglist/queueworker.cpp
new file mode 100644
index 0000000000..0b41a0e395
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/queueworker.cpp
@@ -0,0 +1,68 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "queueworker.h"
+
+/* view needs 8 items and cacheBuffer of 2 items above and below to show visible area, so limit queue max length to that. */
+static constexpr int s_queueMaxLength{12};
+
+QueueWorker::QueueWorker()
+ : QObject{nullptr}
+{
+ connect(this,
+ &QueueWorker::triggerQueueProcessing,
+ this,
+ &QueueWorker::processQueue,
+ Qt::QueuedConnection);
+}
+
+void QueueWorker::abort()
+{
+ m_killSignalReceived = true;
+}
+
+void QueueWorker::fetchData(int id)
+{
+ if (m_indicesAlreadyFetched.contains(id) || m_indicesToFetch.contains(id)
+ || m_killSignalReceived)
+ return;
+
+ /* This is signal from UI thread. It is likely that there are more than one in queue.
+ * Add our own triggerQueueProcessing() signal to the end of event queue and add id to queue of IDs.
+ * Then let all queued signals to be processed until QueueWorker receives its own signal.
+ */
+ sendTriggerQueueProcessingSignal(id);
+}
+
+void QueueWorker::processQueue()
+{
+ /* This slot receives QueueWorker's own signal after all signals from UI thread have been processed. */
+ m_queueProcessingSignalSent = false;
+ const auto index = m_indicesToFetch.dequeue();
+ emit processing(index);
+ const auto data = m_remoteMedia.getElements(1, index);
+ m_indicesAlreadyFetched.insert(index);
+ emit dataFetched(index, data.front());
+ if (!m_indicesToFetch.empty() && !m_killSignalReceived) {
+ /* Queueworker should process more indices, but the UI thread may have sent more signals during previous processing.
+ * Add our own signal to end of event queue and let all preceding signals to be processed before continuing.
+ */
+ emit triggerQueueProcessing(QPrivateSignal{});
+ m_queueProcessingSignalSent = true;
+ }
+ /* Else all fetches have been processed so far. */
+}
+
+void QueueWorker::sendTriggerQueueProcessingSignal(int id)
+{
+ /* Make sure excess IDs are removed. */
+ while (m_indicesToFetch.size() >= s_queueMaxLength) {
+ emit dropped(m_indicesToFetch.dequeue());
+ }
+ /* Before calling this function QueueWorker has ensured that m_indicesToFetch doesn't contain id yet, so just add id. */
+ m_indicesToFetch.enqueue(id);
+ if (!m_queueProcessingSignalSent) {
+ emit triggerQueueProcessing(QPrivateSignal{});
+ m_queueProcessingSignalSent = true;
+ }
+}
diff --git a/examples/quick/models/threadedsonglist/queueworker.h b/examples/quick/models/threadedsonglist/queueworker.h
new file mode 100644
index 0000000000..6a30639d64
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/queueworker.h
@@ -0,0 +1,41 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef QUEUEWORKER_H
+#define QUEUEWORKER_H
+
+#include "remotemedia.h"
+
+#include <QObject>
+#include <QQueue>
+#include <QSet>
+
+class QueueWorker : public QObject
+{
+ Q_OBJECT
+public:
+ explicit QueueWorker();
+ void abort();
+
+signals:
+ void processing(int index);
+ void dropped(int index);
+ void dataFetched(int index, MediaElement element);
+ void triggerQueueProcessing(QPrivateSignal); // signal to self, see implementation of fetchData()
+
+public slots:
+ void fetchData(int index);
+
+private slots:
+ void processQueue();
+
+private:
+ void sendTriggerQueueProcessingSignal(int index);
+ RemoteMedia m_remoteMedia;
+ QSet<int> m_indicesAlreadyFetched;
+ QQueue<int> m_indicesToFetch;
+ std::atomic<bool> m_killSignalReceived{false};
+ bool m_queueProcessingSignalSent{false};
+};
+
+#endif // QUEUEWORKER_H
diff --git a/examples/quick/models/threadedsonglist/remotemedia.cpp b/examples/quick/models/threadedsonglist/remotemedia.cpp
new file mode 100644
index 0000000000..0556458035
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/remotemedia.cpp
@@ -0,0 +1,29 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "remotemedia.h"
+#include "songdatagenerator.h"
+
+#include <QThread>
+
+static constexpr int s_fetchMilliSecondsPerSong{100};
+
+RemoteMedia::RemoteMedia()
+{
+ SongDataGenerator dataGenerator;
+ m_items = dataGenerator.createSongList();
+}
+
+int RemoteMedia::count()
+{
+ return SongDataGenerator::elementMaxCount();
+}
+
+QList<MediaElement> RemoteMedia::getElements(int amount, int offset) const
+{
+ if (amount < 1)
+ return {};
+ QList<MediaElement> returnValue{m_items.cbegin() + offset, m_items.cbegin() + offset + amount};
+ QThread::sleep(std::chrono::milliseconds{s_fetchMilliSecondsPerSong * amount});
+ return returnValue;
+}
diff --git a/examples/quick/models/threadedsonglist/remotemedia.h b/examples/quick/models/threadedsonglist/remotemedia.h
new file mode 100644
index 0000000000..64c081deac
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/remotemedia.h
@@ -0,0 +1,25 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef REMOTEMEDIA_H
+#define REMOTEMEDIA_H
+#include "mediaelement.h"
+
+#include <QList>
+
+class RemoteMedia
+{
+public:
+ explicit RemoteMedia();
+
+ /* Fast access to total count of items (so UI knows the total item list size). */
+ static int count();
+
+ /* Getter (100 milliseconds per item, function call is blocking the thread) */
+ QList<MediaElement> getElements(int amount, int offset) const;
+
+private:
+ QList<MediaElement> m_items;
+};
+
+#endif // REMOTEMEDIA_H
diff --git a/examples/quick/models/threadedsonglist/songdatagenerator.cpp b/examples/quick/models/threadedsonglist/songdatagenerator.cpp
new file mode 100644
index 0000000000..42bf9cfe33
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/songdatagenerator.cpp
@@ -0,0 +1,182 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "songdatagenerator.h"
+
+#include <QStringLiteral>
+
+static constexpr int s_albumCount{6};
+static constexpr int s_bandNamePrefixSuffixCount{5};
+static constexpr int s_songNamePrefixSuffixCount{6};
+static constexpr int s_genreCount{4};
+
+SongDataGenerator::SongDataGenerator()
+{
+ //classical music
+ m_bandPrefixes << QStringList{QStringLiteral("Qtie's"),
+ QStringLiteral("Kjuutburg"),
+ QStringLiteral("Qtional"),
+ QStringLiteral("Kjuutinese"),
+ QStringLiteral("Qtian's")};
+ m_bandSuffixes << QStringList{QStringLiteral("Philharmonic QOrchestra"),
+ QStringLiteral("Theatre QOrchestra"),
+ QStringLiteral("Opera QOrchestra"),
+ QStringLiteral("City QChoir"),
+ QStringLiteral("University QOrchestra")};
+ // metal music
+ m_bandPrefixes << QStringList{QStringLiteral("QtBlack"),
+ QStringLiteral("QtDark"),
+ QStringLiteral("QtHard"),
+ QStringLiteral("QTest"),
+ QStringLiteral("QtPale")};
+ m_bandSuffixes << QStringList{QStringLiteral("Death"),
+ QStringLiteral("Creature"),
+ QStringLiteral("Devil"),
+ QStringLiteral("Souls"),
+ QStringLiteral("Being")};
+ // pop music
+ m_bandPrefixes << QStringList{QStringLiteral("Happy"),
+ QStringLiteral("Joyful"),
+ QStringLiteral("Pretty"),
+ QStringLiteral("Crazy"),
+ QStringLiteral("Wild")};
+ m_bandSuffixes << QStringList{QStringLiteral("QPink"),
+ QStringLiteral("QLife"),
+ QStringLiteral("QGirls"),
+ QStringLiteral("QBoys"),
+ QStringLiteral("QMood")};
+ // rock music
+ m_bandPrefixes << QStringList{QStringLiteral("Grateful"),
+ QStringLiteral("Hard"),
+ QStringLiteral("Rolling"),
+ QStringLiteral("Clashing"),
+ QStringLiteral("Breaking")};
+ m_bandSuffixes << QStringList{QStringLiteral("QML Thunder"),
+ QStringLiteral("QML Slam"),
+ QStringLiteral("QML Rocks"),
+ QStringLiteral("QML Rally"),
+ QStringLiteral("QML Tune")};
+
+ // classical music
+ m_songPrefixes << QStringList{"QWaltz", "QPrelude", "QDance", "QSonata", "QSymphony", "QPiano"};
+ m_songSuffixes << QStringList{"in D Minor",
+ "of the Pixies",
+ "by Night",
+ "on Violin",
+ "no. 5",
+ "in tune"};
+ // metal music
+ m_songPrefixes << QStringList{"QHunger", "QScreaming", "QPain", "QIllusion", "QBeauty", "QDream"};
+ m_songSuffixes << QStringList{"That Never Ends",
+ "Rising",
+ "in My Soul",
+ "from The Dark",
+ "on the Night",
+ "from Beyond"};
+ // pop music
+ m_songPrefixes << QStringList{"Jumping", "Running", "Playing", "Laughing", "Singing", "Joking"};
+ m_songSuffixes << QStringList{"Qt Alone",
+ "of Qt Life",
+ "for Qt Fun",
+ "Q-Day",
+ "with Qties",
+ "All the QTime"};
+ // rock music
+ m_songPrefixes << QStringList{"Full Steam", "Going", "Looking", "Always", "Crazy", "Drive Me"};
+ m_songSuffixes << QStringList{"QWild",
+ "Qt-wards",
+ "to the QEnd",
+ "for you, Qtie",
+ "QTonight",
+ "QHome"};
+ Q_ASSERT(m_bandPrefixes.size() == s_genreCount);
+ Q_ASSERT(m_bandSuffixes.size() == s_genreCount);
+ Q_ASSERT(m_songPrefixes.size() == s_genreCount);
+ Q_ASSERT(m_songSuffixes.size() == s_genreCount);
+ for (const auto &prefixList : std::as_const(m_bandPrefixes)) {
+ Q_ASSERT(prefixList.size() == s_bandNamePrefixSuffixCount);
+ }
+ for (const auto &suffixList : std::as_const(m_bandSuffixes)) {
+ Q_ASSERT(suffixList.size() == s_bandNamePrefixSuffixCount);
+ }
+ for (const auto &prefixList : std::as_const(m_songPrefixes)) {
+ Q_ASSERT(prefixList.size() == s_songNamePrefixSuffixCount);
+ }
+ for (const auto &suffixList : std::as_const(m_songSuffixes)) {
+ Q_ASSERT(suffixList.size() == s_songNamePrefixSuffixCount);
+ }
+}
+
+int SongDataGenerator::elementMaxCount()
+{
+ return s_bandNamePrefixSuffixCount * s_songNamePrefixSuffixCount * s_bandNamePrefixSuffixCount
+ * s_songNamePrefixSuffixCount * s_genreCount;
+}
+
+QList<MediaElement> SongDataGenerator::createSongList() const
+{
+ QList<MediaElement> returnList;
+ returnList.reserve(elementMaxCount());
+ for (int bandSuffix = 0; bandSuffix < s_bandNamePrefixSuffixCount; ++bandSuffix) {
+ for (int songSuffix = 0; songSuffix < s_songNamePrefixSuffixCount; ++songSuffix) {
+ for (int bandPrefix = 0; bandPrefix < s_bandNamePrefixSuffixCount; ++bandPrefix) {
+ for (int songPrefix = 0; songPrefix < s_songNamePrefixSuffixCount; ++songPrefix) {
+ for (int genre = 0; genre < s_genreCount; ++genre) {
+ returnList.append(
+ MediaElement{songName(songPrefix, songSuffix, genre),
+ bandName(bandPrefix, bandSuffix, genre),
+ SongDataGenerator::albumName(bandPrefix, songPrefix, genre),
+ SongDataGenerator::albumArt(bandPrefix, songPrefix, genre)});
+ }
+ }
+ }
+ }
+ }
+ Q_ASSERT(returnList.size() == elementMaxCount());
+ return returnList;
+}
+
+QString SongDataGenerator::bandName(int bandNamePrefixIndex,
+ int bandNameSuffixIndex,
+ int genreIndex) const
+{
+ return QStringLiteral("%1 %2").arg(m_bandPrefixes.at(genreIndex).at(bandNamePrefixIndex),
+ m_bandSuffixes.at(genreIndex).at(bandNameSuffixIndex));
+}
+
+QString SongDataGenerator::songName(int songNamePrefixIndex,
+ int songNameSuffixIndex,
+ int genreIndex) const
+{
+ return QStringLiteral("%1 %2").arg(m_songPrefixes.at(genreIndex).at(songNamePrefixIndex),
+ m_songSuffixes.at(genreIndex).at(songNameSuffixIndex));
+}
+
+QString SongDataGenerator::albumName(int bandNamePrefixIndex,
+ int songNamePrefixIndex,
+ int genreIndex)
+{
+ static const QStringList albums{"First QAlbum",
+ "The QBest Of",
+ "World QtTour",
+ "Live at Qt Arena",
+ "QEnchanted",
+ "QFarewell"};
+ Q_ASSERT(albums.size() == s_albumCount);
+ return albums.at(albumIndex(bandNamePrefixIndex, songNamePrefixIndex, genreIndex));
+}
+
+QUrl SongDataGenerator::albumArt(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex)
+{
+ static const QStringList genres{"classical", "metal", "pop", "rock"};
+ return QUrl{QStringLiteral("qrc:/qt/qml/threadedsonglist/images/album_covers/%1%2.jpeg")
+ .arg(genres.at(genreIndex))
+ .arg(albumIndex(bandNamePrefixIndex, songNamePrefixIndex, genreIndex) + 1)};
+}
+
+int SongDataGenerator::albumIndex(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex)
+{
+ return ((bandNamePrefixIndex * s_songNamePrefixSuffixCount) + songNamePrefixIndex
+ + (genreIndex % s_genreCount))
+ % s_albumCount;
+}
diff --git a/examples/quick/models/threadedsonglist/songdatagenerator.h b/examples/quick/models/threadedsonglist/songdatagenerator.h
new file mode 100644
index 0000000000..9227a5badb
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/songdatagenerator.h
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef SONGDATAGENERATOR_H
+#define SONGDATAGENERATOR_H
+#include "mediaelement.h"
+
+#include <QPair>
+#include <QStringList>
+
+class SongDataGenerator
+{
+public:
+ explicit SongDataGenerator();
+ static int elementMaxCount();
+ QList<MediaElement> createSongList() const;
+
+private:
+ QString bandName(int bandNamePrefixIndex, int bandNameSuffixIndex, int genreIndex) const;
+ QString songName(int songNamePrefixIndex, int songNameSuffixIndex, int genreIndex) const;
+ static QString albumName(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex);
+ static QUrl albumArt(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex);
+ static int albumIndex(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex);
+ QList<QStringList> m_bandPrefixes;
+ QList<QStringList> m_bandSuffixes;
+ QList<QStringList> m_songPrefixes;
+ QList<QStringList> m_songSuffixes;
+};
+
+#endif // SONGDATAGENERATOR_H
diff --git a/examples/quick/models/threadedsonglist/threadedlistmodel.cpp b/examples/quick/models/threadedsonglist/threadedlistmodel.cpp
new file mode 100644
index 0000000000..b509d88ead
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/threadedlistmodel.cpp
@@ -0,0 +1,75 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "threadedlistmodel.h"
+
+ThreadedListModel::ThreadedListModel()
+{
+ m_idList = m_storage.idList();
+ connect(&m_storage, &DataStorage::dataUpdated, this, &ThreadedListModel::dataUpdated);
+}
+
+int ThreadedListModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return m_idList.size();
+}
+
+QVariant ThreadedListModel::data(const QModelIndex &index, int role) const
+{
+ const auto id = m_idList.at(index.row());
+ const auto &item = m_storage.item(id);
+ if (item.isValid()) {
+ switch (static_cast<Role>(role)) {
+ case Role::Album:
+ return item.album();
+ case Role::AlbumArt:
+ return item.albumArtFile();
+ case Role::Artist:
+ return item.artist();
+ case Role::Song:
+ return item.song();
+ case Role::LoadingElement:
+ return false;
+ case Role::LoadingText:
+ return QString();
+ default:
+ break;
+ }
+ } else {
+ /* Item not fetched yet, pass "loading" item. */
+ switch (static_cast<Role>(role)) {
+ case Role::Album:
+ case Role::AlbumArt:
+ case Role::Artist:
+ case Role::Song:
+ return QString{};
+ case Role::LoadingElement:
+ return true;
+ case Role::LoadingText:
+ return m_storage.currentlyFetchedId() == id ? QString{} : tr("Waiting...");
+ default:
+ break;
+ }
+ }
+ return QVariant{};
+}
+
+void ThreadedListModel::dataUpdated(int id)
+{
+ const QModelIndex changedIndex = index(m_idList.indexOf(id));
+ emit dataChanged(changedIndex, changedIndex);
+}
+
+QHash<int, QByteArray> ThreadedListModel::roleNames() const
+{
+ static const QHash<int, QByteArray> roles{{static_cast<int>(Role::Song), "song"},
+ {static_cast<int>(Role::Artist), "artist"},
+ {static_cast<int>(Role::Album), "album"},
+ {static_cast<int>(Role::AlbumArt), "albumArt"},
+ {static_cast<int>(Role::LoadingText), "loadingText"},
+ {static_cast<int>(Role::LoadingElement),
+ "isLoadingElement"}};
+ return roles;
+}
diff --git a/examples/quick/models/threadedsonglist/threadedlistmodel.h b/examples/quick/models/threadedsonglist/threadedlistmodel.h
new file mode 100644
index 0000000000..b9d4056b15
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/threadedlistmodel.h
@@ -0,0 +1,41 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef THREADEDLISTMODEL_H
+#define THREADEDLISTMODEL_H
+
+#include "datastorage.h"
+
+#include <QAbstractListModel>
+#include <QQmlEngine>
+
+class ThreadedListModel : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+public:
+ explicit ThreadedListModel();
+ Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+protected:
+ enum class Role {
+ Song = Qt::ItemDataRole::UserRole,
+ Artist,
+ Album,
+ AlbumArt,
+ LoadingElement,
+ LoadingText
+ };
+
+private slots:
+ void dataUpdated(int id);
+
+private:
+ DataStorage m_storage;
+ QList<int> m_idList;
+ int m_currentSongId{-1};
+};
+
+#endif // THREADEDLISTMODEL_H
diff --git a/examples/quick/models/threadedsonglist/threadedsonglist.pro b/examples/quick/models/threadedsonglist/threadedsonglist.pro
new file mode 100644
index 0000000000..bc83d7c89f
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/threadedsonglist.pro
@@ -0,0 +1,20 @@
+TARGET = threadedsonglist
+QT += qml quick
+
+HEADERS = datastorage.h \
+ mediaelement.h \
+ queueworker.h \
+ remotemedia.h \
+ songdatagenerator.h \
+ threadedlistmodel.h
+SOURCES = main.cpp \
+ datastorage.cpp \
+ mediaelement.cpp \
+ queueworker.cpp \
+ remotemedia.cpp \
+ songdatagenerator.cpp \
+ threadedlistmodel.cpp
+RESOURCES += threadedsonglist.qrc
+
+target.path = $$[QT_INSTALL_EXAMPLES]/quick/models/threadedsonglist
+INSTALLS += target
diff --git a/examples/quick/models/threadedsonglist/threadedsonglist.qrc b/examples/quick/models/threadedsonglist/threadedsonglist.qrc
new file mode 100644
index 0000000000..ac4cd3c049
--- /dev/null
+++ b/examples/quick/models/threadedsonglist/threadedsonglist.qrc
@@ -0,0 +1,32 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource prefix="/">
+ <file>SongListDelegate.qml</file>
+ <file>ThreadedSongList.qml</file>
+ <file>images/album_covers/classical1.jpeg</file>
+ <file>images/album_covers/classical2.jpeg</file>
+ <file>images/album_covers/classical3.jpeg</file>
+ <file>images/album_covers/classical4.jpeg</file>
+ <file>images/album_covers/classical5.jpeg</file>
+ <file>images/album_covers/classical6.jpeg</file>
+ <file>images/album_covers/metal1.jpeg</file>
+ <file>images/album_covers/metal2.jpeg</file>
+ <file>images/album_covers/metal3.jpeg</file>
+ <file>images/album_covers/metal4.jpeg</file>
+ <file>images/album_covers/metal5.jpeg</file>
+ <file>images/album_covers/metal6.jpeg</file>
+ <file>images/album_covers/pop1.jpeg</file>
+ <file>images/album_covers/pop2.jpeg</file>
+ <file>images/album_covers/pop3.jpeg</file>
+ <file>images/album_covers/pop4.jpeg</file>
+ <file>images/album_covers/pop5.jpeg</file>
+ <file>images/album_covers/pop6.jpeg</file>
+ <file>images/album_covers/rock1.jpeg</file>
+ <file>images/album_covers/rock2.jpeg</file>
+ <file>images/album_covers/rock3.jpeg</file>
+ <file>images/album_covers/rock4.jpeg</file>
+ <file>images/album_covers/rock5.jpeg</file>
+ <file>images/album_covers/rock6.jpeg</file>
+ <file>images/device/remote.jpeg</file>
+</qresource>
+</RCC>
+