aboutsummaryrefslogtreecommitdiffstats
path: root/examples/tutorials
diff options
context:
space:
mode:
authorJaime Resano <Jaime.Resano-Aisa@qt.io>2025-03-10 17:06:36 +0100
committerCristián Maureira-Fredes <Cristian.Maureira-Fredes@qt.io>2025-07-29 14:12:10 +0200
commitefd8944b263a099e901ab588bc2d9357554f6ead (patch)
tree17e30db34633188558e5878535f6e8b86d9a7895 /examples/tutorials
parent7ddd042bf3080ac354ea5c673ba244ea0796117e (diff)
Add Qt Design Studio tutorial
Add a new Qt Design Studio tutorial to the documentation. The "Drumpad" example is added to the examples/tutorials folder, with both initial and final projects. The required Sounds are in a directory at the same level as both tutorials in order to reduce space. Change-Id: I59fdd662775ae48ee04e663d4aa5aa8cb333fcd5 Reviewed-by: Shyamnath Premnadh <Shyamnath.Premnadh@qt.io>
Diffstat (limited to 'examples/tutorials')
-rw-r--r--examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wavbin0 -> 553142 bytes
-rw-r--r--examples/tutorials/drumpad/Sounds/Clap.wavbin0 -> 69302 bytes
-rw-r--r--examples/tutorials/drumpad/Sounds/Closed Hat.wavbin0 -> 69302 bytes
-rw-r--r--examples/tutorials/drumpad/Sounds/Kick Drum.wavbin0 -> 69302 bytes
-rw-r--r--examples/tutorials/drumpad/Sounds/Open Hat.wavbin0 -> 207542 bytes
-rw-r--r--examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wavbin0 -> 276662 bytes
-rw-r--r--examples/tutorials/drumpad/final_project/.gitignore11
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad.qmlproject69
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad.qrc23
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml111
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml22
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/Constants.qml12
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml110
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml118
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml68
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml39
-rw-r--r--examples/tutorials/drumpad/final_project/Drumpad/qmldir10
-rw-r--r--examples/tutorials/drumpad/final_project/DrumpadContent/App.qml21
-rw-r--r--examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml99
-rw-r--r--examples/tutorials/drumpad/final_project/DrumpadContent/qmldir4
-rw-r--r--examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml27
-rw-r--r--examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml13
-rw-r--r--examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir3
-rw-r--r--examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml8
-rw-r--r--examples/tutorials/drumpad/final_project/Mocks/Components/qmldir2
-rw-r--r--examples/tutorials/drumpad/final_project/Python/audio/__init__.py6
-rw-r--r--examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py65
-rw-r--r--examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py29
-rw-r--r--examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py113
-rw-r--r--examples/tutorials/drumpad/final_project/Python/autogen/settings.py39
-rw-r--r--examples/tutorials/drumpad/final_project/Python/main.py28
-rw-r--r--examples/tutorials/drumpad/final_project/Python/pyproject.toml5
-rw-r--r--examples/tutorials/drumpad/final_project/doc/final_project.md12
-rw-r--r--examples/tutorials/drumpad/final_project/qtquickcontrols2.conf6
-rw-r--r--examples/tutorials/drumpad/initial_project/.gitignore11
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad.qmlproject69
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad.qrc23
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml111
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml22
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml12
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml110
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml118
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml68
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml39
-rw-r--r--examples/tutorials/drumpad/initial_project/Drumpad/qmldir10
-rw-r--r--examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml21
-rw-r--r--examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml99
-rw-r--r--examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir4
-rw-r--r--examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml27
-rw-r--r--examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml13
-rw-r--r--examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir3
-rw-r--r--examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml8
-rw-r--r--examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir2
-rw-r--r--examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject21
-rw-r--r--examples/tutorials/drumpad/initial_project/doc/initial_project.md12
-rw-r--r--examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf6
56 files changed, 1882 insertions, 0 deletions
diff --git a/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav b/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav
new file mode 100644
index 000000000..b90bc45e8
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Bongo Loop 125bpm.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/Sounds/Clap.wav b/examples/tutorials/drumpad/Sounds/Clap.wav
new file mode 100644
index 000000000..aceee331c
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Clap.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/Sounds/Closed Hat.wav b/examples/tutorials/drumpad/Sounds/Closed Hat.wav
new file mode 100644
index 000000000..d062e723a
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Closed Hat.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/Sounds/Kick Drum.wav b/examples/tutorials/drumpad/Sounds/Kick Drum.wav
new file mode 100644
index 000000000..e28337136
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Kick Drum.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/Sounds/Open Hat.wav b/examples/tutorials/drumpad/Sounds/Open Hat.wav
new file mode 100644
index 000000000..e6fcb130c
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Open Hat.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav b/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav
new file mode 100644
index 000000000..5925d0fcf
--- /dev/null
+++ b/examples/tutorials/drumpad/Sounds/Sine Bass Ebm.wav
Binary files differ
diff --git a/examples/tutorials/drumpad/final_project/.gitignore b/examples/tutorials/drumpad/final_project/.gitignore
new file mode 100644
index 000000000..855f31da5
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/.gitignore
@@ -0,0 +1,11 @@
+__pycache__/
+.DS_Store
+build/
+deployment/
+pysidedeploy.spec
+resources.py
+*.autosave
+*.dist/
+Dependencies/
+*.qtds
+.qmlls.ini
diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qmlproject b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject
new file mode 100644
index 000000000..591606bce
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad.qmlproject
@@ -0,0 +1,69 @@
+// prop: json-converted
+// prop: auto-generated
+
+import QmlProject
+
+Project {
+ mainFile: "DrumpadContent/App.qml"
+ mainUiFile: "DrumpadContent/MainScreen.qml"
+ targetDirectory: "/opt/Drumpad"
+ enableCMakeGeneration: false
+ enablePythonGeneration: true
+ widgetApp: true
+ importPaths: [ "." ]
+ mockImports: [ "Mocks" ]
+
+ qdsVersion: "4.5"
+ quickVersion: "6.7"
+ qt6Project: true
+ qtForMCUs: false
+
+ multilanguageSupport: true
+ primaryLanguage: "en"
+ supportedLanguages: [ "en" ]
+
+ Environment {
+ QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1"
+ QT_AUTO_SCREEN_SCALE_FACTOR: "1"
+ QT_ENABLE_HIGHDPI_SCALING: "0"
+ QT_LOGGING_RULES: "qt.qml.connections=false"
+ QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf"
+ }
+
+ QmlFiles {
+ directory: "Drumpad"
+ }
+
+ QmlFiles {
+ directory: "DrumpadContent"
+ }
+
+ QmlFiles {
+ directory: "Generated"
+ }
+
+ Files {
+ directory: "../Sounds"
+ filter: "*.mp3;*.wav"
+ }
+
+ QmlFiles {
+ directory: "Mocks/Audio"
+ }
+
+ Files {
+ files: [
+ "qtquickcontrols2.conf"
+ ]
+ }
+
+ Files {
+ directory: "Drumpad"
+ filter: "qmldir"
+ }
+
+ Files {
+ directory: "DrumpadContent"
+ filter: "*.ttf;*.otf"
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad.qrc b/examples/tutorials/drumpad/final_project/Drumpad.qrc
new file mode 100644
index 000000000..b856a46a9
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad.qrc
@@ -0,0 +1,23 @@
+<RCC>
+ <qresource>
+ <file>Drumpad.qmlproject</file>
+ <file>Drumpad/AvailableSoundsComboBox.qml</file>
+ <file>Drumpad/CenteredFlow.qml</file>
+ <file>Drumpad/Constants.qml</file>
+ <file>Drumpad/PadButton.qml</file>
+ <file>Drumpad/qmldir</file>
+ <file>Drumpad/SoundEffectPlayer.qml</file>
+ <file>Drumpad/StyledSpinBox.qml</file>
+ <file>Drumpad/VolumeSlider.qml</file>
+ <file>DrumpadContent/App.qml</file>
+ <file>DrumpadContent/MainScreen.qml</file>
+ <file>DrumpadContent/qmldir</file>
+ <file>qtquickcontrols2.conf</file>
+ <file>../Sounds/Bongo Loop 125bpm.wav</file>
+ <file>../Sounds/Clap.wav</file>
+ <file>../Sounds/Closed Hat.wav</file>
+ <file>../Sounds/Kick Drum.wav</file>
+ <file>../Sounds/Open Hat.wav</file>
+ <file>../Sounds/Sine Bass Ebm.wav</file>
+ </qresource>
+</RCC>
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml
new file mode 100644
index 000000000..e0e6a72ba
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/AvailableSoundsComboBox.qml
@@ -0,0 +1,111 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtQuick.Controls
+import Audio
+
+ComboBox {
+ id: root
+
+ property string currentFile: currentText ? `../Sounds/${currentText}` : ""
+ required property int initialIndex
+
+ model: audioFilesModel.getModel()
+
+ background: Rectangle {
+ border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor
+ border.width: root.visualFocus ? 3 : 2
+ color: root.pressed ? Constants.secondaryColor : "black"
+ implicitHeight: 30
+ radius: 2
+ }
+ contentItem: Text {
+ color: "white"
+ elide: Text.ElideRight
+ leftPadding: 10
+ rightPadding: root.indicator.width + 10
+ text: root.displayText
+ verticalAlignment: Text.AlignVCenter
+ }
+ delegate: ItemDelegate {
+ id: delegate
+
+ required property int index
+
+ highlighted: root.highlightedIndex === index
+
+ background: Rectangle {
+ color: delegate.highlighted ? Constants.darkGray : "black"
+ implicitWidth: delegate.contentItem.implicitWidth
+ width: popup.width
+ }
+ contentItem: Text {
+ anchors.fill: parent
+ color: delegate.highlighted ? "#ff0000" : "white"
+ elide: Text.ElideRight
+ leftPadding: 10
+ text: root.model[delegate.index]
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ indicator: Canvas {
+ id: canvas
+
+ contextType: "2d"
+ height: 8
+ width: 12
+ x: root.width - canvas.width - root.rightPadding
+ y: root.topPadding + (root.availableHeight - canvas.height) / 2
+
+ onPaint: {
+ let margin = 2;
+ context.reset();
+ context.lineWidth = 2;
+ context.strokeStyle = "white";
+ context.lineCap = "round";
+ context.beginPath();
+ context.moveTo(margin, margin);
+ context.lineTo(width / 2, height - margin);
+ context.lineTo(width - margin, margin);
+ context.stroke();
+ }
+
+ Connections {
+ function onPressedChanged() {
+ canvas.requestPaint();
+ }
+
+ target: root
+ }
+ }
+ popup: Popup {
+ id: popup
+
+ implicitHeight: contentItem.implicitHeight
+ implicitWidth: 200
+ padding: 2
+ y: root.height + 2
+
+ background: Rectangle {
+ border.color: Constants.primaryColor
+ border.width: 2
+ color: "black"
+ }
+ contentItem: ListView {
+ clip: true
+ currentIndex: root.highlightedIndex
+ implicitHeight: Math.min(contentHeight, 200)
+ model: popup.visible ? root.delegateModel : null
+ }
+ }
+
+ Component.onCompleted: {
+ currentIndex = root.initialIndex % model.length;
+ }
+
+ AudioFilesModel {
+ id: audioFilesModel
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml
new file mode 100644
index 000000000..44911c1b2
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/CenteredFlow.qml
@@ -0,0 +1,22 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+// A Flow layout that centers its children horizontally
+// Note that the implementation adds unnecessary spacing in rows that are not full
+Flow {
+ property int customMargin: (children.length && (children[0].width + spacing <= parentWidth))
+ ? (parentWidth - rowWidth) / 2 + padding
+ : padding
+ property int parentWidth: parent.width - 2 * padding
+ property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0
+ property int rowWidth: children.length
+ ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding
+ : 0
+
+ anchors {
+ leftMargin: customMargin
+ rightMargin: customMargin
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml
new file mode 100644
index 000000000..6afab9c87
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/Constants.qml
@@ -0,0 +1,12 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+pragma Singleton
+import QtQuick
+
+QtObject {
+ readonly property string darkGray: "#333333"
+ readonly property string mediumGray: "#9B9B9B"
+ readonly property string primaryColor: "#FF0000"
+ readonly property string secondaryColor: "#8C0000"
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml
new file mode 100644
index 000000000..b95642692
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/PadButton.qml
@@ -0,0 +1,110 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Shapes
+
+Rectangle {
+ id: root
+
+ property bool isPlaying: false
+ property bool isError: false
+ property bool isLoading: false
+ property int cornerRadius: 10
+ signal pressed()
+
+ color: "transparent"
+
+ Shape {
+ anchors.fill: parent
+
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 2
+
+ fillGradient: RadialGradient {
+ centerRadius: root.height
+ centerX: root.width / 2
+ centerY: root.height / 2
+ focalX: centerX
+ focalY: centerY
+
+ GradientStop {
+ position: 0
+ color: {
+ if (isError)
+ return "black";
+ if (isLoading)
+ return "yellow";
+ if (isPlaying)
+ return Qt.darker(Constants.primaryColor, 1.25);
+ return Qt.darker(Constants.secondaryColor, 1.25);
+ }
+ }
+ GradientStop {
+ position: 0.5
+ color: {
+ if (isError)
+ return Constants.darkGray;
+ if (isLoading)
+ return "orange";
+ if (isPlaying)
+ return Constants.primaryColor;
+ return Constants.secondaryColor;
+ }
+ }
+ }
+
+ // Rounded shape path
+ PathMove {
+ x: root.cornerRadius
+ y: 0
+ }
+ PathQuad {
+ controlX: 0
+ controlY: 0
+ x: 0
+ y: root.cornerRadius
+ }
+ PathLine {
+ x: 0
+ y: root.height - root.cornerRadius
+ }
+ PathQuad {
+ controlX: 0
+ controlY: root.height
+ x: root.cornerRadius
+ y: root.height
+ }
+ PathLine {
+ x: root.width - root.cornerRadius
+ y: root.height
+ }
+ PathQuad {
+ controlX: root.width
+ controlY: root.height
+ x: root.width
+ y: root.height - root.cornerRadius
+ }
+ PathLine {
+ x: root.width
+ y: root.cornerRadius
+ }
+ PathQuad {
+ controlX: root.width
+ controlY: 0
+ x: root.width - root.cornerRadius
+ y: 0
+ }
+ PathLine {
+ x: root.cornerRadius
+ y: 0
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.pressed()
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml
new file mode 100644
index 000000000..a50b3306f
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/SoundEffectPlayer.qml
@@ -0,0 +1,118 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import QtMultimedia
+
+import Drumpad
+import Audio
+
+Rectangle {
+ id: root
+
+ property string decodingError: ""
+ required property int index
+ property int status: SoundEffect.Null
+ property bool isLoading: status == SoundEffect.Loading
+ property bool isError: status == SoundEffect.Error || status == SoundEffect.Null
+ property bool isReady: status == SoundEffect.Ready
+
+ function play() {
+ if (root.status == SoundEffect.Ready) {
+ audioEngine.play();
+ }
+ }
+
+ color: Constants.darkGray
+ implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins
+ implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins
+ radius: 10
+
+ onDecodingErrorChanged: {
+ if (status == SoundEffect.Error && root.decodingError) {
+ errorMessageDialog.text = root.decodingError;
+ errorMessageDialog.open();
+ }
+ }
+
+ AudioEngine {
+ id: audioEngine
+
+ file: availableSoundsComboBox.currentFile
+ volume: volumeSlider.value
+
+ onDecodingStatusChanged: (status, error) => {
+ root.status = status;
+ if (status == SoundEffect.Error && error) {
+ root.decodingError = error;
+ } else {
+ root.decodingError = "";
+ }
+ }
+ }
+
+ MessageDialog {
+ id: errorMessageDialog
+
+ buttons: MessageDialog.Ok
+ title: "Error decoding file"
+ }
+
+ ColumnLayout {
+ id: layout
+
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 10
+
+ RowLayout {
+ spacing: 10
+
+ Text {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ color: "white"
+ text: `Player ${root.index + 1}`
+ }
+ AvailableSoundsComboBox {
+ id: availableSoundsComboBox
+
+ Layout.alignment: Qt.AlignCenter
+ initialIndex: root.index
+ }
+ }
+
+ WaveformItem {
+ id: waveformItem
+
+ file: audioEngine.file
+ height: 100
+ width: 300
+ }
+
+ Row {
+ Layout.alignment: Qt.AlignCenter
+ spacing: 10
+
+ PadButton {
+ id: padRectangle
+ height: 100
+ width: 100
+ isPlaying: audioEngine.isPlaying
+ isError: root.isError
+ isLoading: root.isLoading
+ onPressed: root.play()
+ }
+
+ VolumeSlider {
+ id: volumeSlider
+
+ height: padRectangle.height
+ value: 0.75
+ width: 16
+ }
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml
new file mode 100644
index 000000000..de95412bb
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/StyledSpinBox.qml
@@ -0,0 +1,68 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+
+SpinBox {
+ id: root
+
+ property int innerPadding: 10
+
+ height: contentItem.implicitHeight + innerPadding
+ width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth
+
+ background: Rectangle {
+ border.color: Constants.secondaryColor
+ }
+
+ contentItem: Text {
+ color: "black"
+ height: parent.height
+ horizontalAlignment: Text.AlignHCenter
+ text: root.textFromValue(root.value, root.locale)
+ verticalAlignment: Text.AlignVCenter
+ width: implicitWidth + innerPadding * 2
+ }
+
+ down.indicator: Rectangle {
+ border.color: Constants.secondaryColor
+ color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black"
+ height: parent.height
+ implicitWidth: downText.implicitWidth + innerPadding * 2
+ x: root.mirrored ? parent.width - width : 0
+
+ Text {
+ id: downText
+
+ anchors.fill: parent
+ color: "white"
+ font.pixelSize: Math.round(root.font.pixelSize * 1.5)
+ fontSizeMode: Text.Fit
+ horizontalAlignment: Text.AlignHCenter
+ text: "-"
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ up.indicator: Rectangle {
+ border.color: Constants.secondaryColor
+ color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black"
+ height: parent.height
+ implicitWidth: upText.implicitWidth + innerPadding * 2
+ x: root.mirrored ? 0 : parent.width - width
+
+ Text {
+ id: upText
+
+ anchors.centerIn: parent
+ anchors.fill: parent
+ color: "white"
+ font.pixelSize: Math.round(root.font.pixelSize * 1.5)
+ fontSizeMode: Text.Fit
+ horizontalAlignment: Text.AlignHCenter
+ text: "+"
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml
new file mode 100644
index 000000000..102291213
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/VolumeSlider.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+
+Slider {
+ id: root
+
+ orientation: Qt.Vertical
+ padding: 0
+
+ background: Rectangle {
+ color: Constants.mediumGray
+ implicitHeight: root.height
+ implicitWidth: root.width
+ radius: width / 2
+
+ Rectangle {
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3))
+ height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height)
+ radius: parent.width / 2
+ width: parent.width
+ }
+ }
+
+ handle: Rectangle {
+ border.color: "#b0b0b0"
+ border.width: 1
+ color: root.pressed ? "#e0e0e0" : "#ffffff"
+ height: root.width
+ radius: width / 2
+ width: root.width
+ x: root.availableWidth / 2 - height / 2
+ y: root.visualPosition * (root.availableHeight - height)
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Drumpad/qmldir b/examples/tutorials/drumpad/final_project/Drumpad/qmldir
new file mode 100644
index 000000000..7dba78c17
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Drumpad/qmldir
@@ -0,0 +1,10 @@
+module Drumpad
+
+AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml
+SoundEffectPlayer 1.0 SoundEffectPlayer.qml
+CenteredFlow 1.0 CenteredFlow.qml
+VolumeSlider 1.0 VolumeSlider.qml
+StyledSpinBox 1.0 StyledSpinBox.qml
+PadButton 1.0 PadButton.qml
+
+singleton Constants 1.0 Constants.qml
diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml
new file mode 100644
index 000000000..773cbbfc8
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/DrumpadContent/App.qml
@@ -0,0 +1,21 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import Drumpad 1.0
+
+Window {
+ id: root
+
+ height: 800
+ title: "Drumpad"
+ visible: true
+ width: 1200
+
+ MainScreen {
+ id: mainScreen
+
+ anchors.fill: parent
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml
new file mode 100644
index 000000000..fdbd7b66d
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/DrumpadContent/MainScreen.qml
@@ -0,0 +1,99 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Drumpad
+import Audio
+
+Rectangle {
+ id: root
+
+ property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml",
+ Component.PreferSynchronous)
+
+ color: "black"
+ focus: true
+
+ Component.onCompleted: {
+ // Initialize the default sound effect players
+ for (var i = 0; i < audioPlayersSpinBox.value; i++) {
+ root.soundEffectPlayer.createObject(soundEffectPlayersFlow, {
+ index: i
+ });
+ }
+ }
+ Keys.onPressed: event => {
+ if (event.key < Qt.Key_1 || event.key > Qt.Key_9) {
+ // Ignore key out of scope
+ return;
+ }
+
+ let digit = event.key - Qt.Key_1;
+ if (digit < soundEffectPlayersFlow.children.length) {
+ soundEffectPlayersFlow.children[digit].play();
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Row {
+ id: audioPlayersCountRow
+
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 5
+
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ color: "white"
+ text: "Audio players:"
+ }
+
+ StyledSpinBox {
+ id: audioPlayersSpinBox
+
+ value: 5
+
+ onValueModified: {
+ let soundPlayersCount = soundEffectPlayersFlow.children.length;
+ if (audioPlayersSpinBox.value < soundPlayersCount) {
+ // Remove extra sound effect players
+ soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value;
+ return;
+ }
+
+ if (audioPlayersSpinBox.value < soundPlayersCount) {
+ return;
+ }
+ // Create more sound effect players
+ for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) {
+ root.soundEffectPlayer.createObject(soundEffectPlayersFlow, {
+ index: i
+ });
+ }
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ contentWidth: width
+
+ background: Rectangle {
+ color: "#232323"
+ }
+
+ CenteredFlow {
+ id: soundEffectPlayersFlow
+
+ anchors.fill: parent
+ padding: 10
+ spacing: 10
+ }
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir
new file mode 100644
index 000000000..f1f34c528
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/DrumpadContent/qmldir
@@ -0,0 +1,4 @@
+module DrumpadContent
+
+App 1.0 App.qml
+MainScreen 1.0 MainScreen.qml
diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml
new file mode 100644
index 000000000..4bfbc24f3
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/AudioEngine.qml
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtMultimedia
+
+Item {
+ id: root
+
+ property double volume
+ property url file
+
+ MediaPlayer {
+ id: player
+ source: file
+ audioOutput: AudioOutput {}
+ }
+
+ onVolumeChanged : {
+ console.log("Mock: VolumeChanaged ", volume )
+ }
+
+ function play() {
+ console.log("Mock: play()")
+ player.play()
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml
new file mode 100644
index 000000000..fcb6041bb
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/WaveformItem.qml
@@ -0,0 +1,13 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ id: root
+ width: 1920
+ height: 1080
+ color: "blue"
+ property url file
+}
diff --git a/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir
new file mode 100644
index 000000000..189d68e6b
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Mocks/Audio/qmldir
@@ -0,0 +1,3 @@
+module Audio
+AudioEngine 1.0 AudioEngine.qml
+WaveformItem 1.0 WaveformItem.qml
diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml
new file mode 100644
index 000000000..7e4ff22ad
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Mocks/Components/AudioFilesModel.qml
@@ -0,0 +1,8 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+Item {
+ getFiles: function() {
+ console.log("AudioFilesModel mock: getFiles()")
+ }
+}
diff --git a/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir
new file mode 100644
index 000000000..2d8e98995
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Mocks/Components/qmldir
@@ -0,0 +1,2 @@
+module Components
+AudioFilesModel 1.0 AudioFilesModel.qml
diff --git a/examples/tutorials/drumpad/final_project/Python/audio/__init__.py b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py
new file mode 100644
index 000000000..817bc3e1e
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/audio/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from .audio_engine import AudioEngine
+from .waveform_item import WaveformItem
+from .audio_files_model import AudioFilesModel
diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py
new file mode 100644
index 000000000..daf2b356c
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_engine.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtQml import QmlElement
+from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl
+from PySide6.QtMultimedia import QSoundEffect
+
+from autogen.settings import project_root
+
+QML_IMPORT_NAME = "Audio"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class AudioEngine(QObject):
+ volumeChanged = Signal()
+ fileChanged = Signal()
+ isPlayingChanged = Signal()
+ decodingStatusChanged = Signal(QSoundEffect.Status, str)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._sound_effect = QSoundEffect()
+ self._sound_effect.playingChanged.connect(self.isPlayingChanged.emit) #
+ self._sound_effect.statusChanged.connect(self.reportStatus)
+
+ def reportStatus(self):
+ if self._sound_effect.status() == QSoundEffect.Status.Error:
+ self.decodingStatusChanged.emit(
+ QSoundEffect.Status.Error,
+ f"Error decoding file: {self._sound_effect.source().path()}",
+ )
+ else:
+ self.decodingStatusChanged.emit(self._sound_effect.status(), "")
+
+ @Slot(result=None)
+ def play(self):
+ self._sound_effect.play()
+
+ def volume(self):
+ return self._sound_effect.volume()
+
+ def setVolume(self, value):
+ self._sound_effect.setVolume(value)
+ self.volumeChanged.emit()
+
+ def file(self):
+ return self._sound_effect.source()
+
+ def setFile(self, value: QUrl):
+ if self._sound_effect.source() == value or value.isEmpty():
+ return
+
+ if "__compiled__" in globals():
+ self._sound_effect.setSource(f"qrc:/{value.toString()}")
+ else:
+ self._sound_effect.setSource(f"file:{project_root / value.toString()}")
+ self.fileChanged.emit()
+
+ def isPlaying(self):
+ return self._sound_effect.isPlaying()
+
+ volume = Property(float, volume, setVolume, notify=volumeChanged)
+ file = Property(QUrl, file, setFile, notify=fileChanged)
+ isPlaying = Property(bool, isPlaying, notify=isPlayingChanged)
diff --git a/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py
new file mode 100644
index 000000000..bdf7cd61e
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/audio/audio_files_model.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from pathlib import Path
+
+from PySide6.QtCore import QObject, Slot, QDirIterator
+from PySide6.QtQml import QmlElement
+
+from autogen.settings import project_root
+
+
+QML_IMPORT_NAME = "Audio"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class AudioFilesModel(QObject):
+ @Slot(result=list)
+ def getModel(self):
+ if "__compiled__" in globals():
+ resource_prefix = ":/Sounds/"
+ iterator = QDirIterator(resource_prefix, QDirIterator.Subdirectories)
+ audio_files = []
+ while iterator.hasNext():
+ resource = iterator.next()
+ audio_files.append(resource.split(resource_prefix)[-1])
+ return audio_files
+
+ return list(p.name for p in Path(project_root / ".." / "Sounds").glob("*.wav"))
diff --git a/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py
new file mode 100644
index 000000000..d3ce0f430
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/audio/waveform_item.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import struct
+
+from PySide6.QtCore import Qt, Property, QUrl, Signal, QFile, QPointF
+from PySide6.QtGui import QPen, QPainter
+from PySide6.QtMultimedia import QAudioFormat, QAudioDecoder
+from PySide6.QtQml import QmlElement
+from PySide6.QtQuick import QQuickPaintedItem
+
+QML_IMPORT_NAME = "Audio"
+QML_IMPORT_MAJOR_VERSION = 1
+
+
+@QmlElement
+class WaveformItem(QQuickPaintedItem):
+
+ fileChanged = Signal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._waveformData = []
+ self._background_color = Qt.black
+
+ audio_format = QAudioFormat()
+ audio_format.setChannelCount(1)
+ audio_format.setSampleRate(44100)
+ audio_format.setSampleFormat(QAudioFormat.Float)
+
+ self._file_url: QUrl | None = None
+ self._audio_file: QFile | None = None
+
+ self._decoder = QAudioDecoder()
+ self._decoder.setAudioFormat(audio_format)
+
+ self._decoder.bufferReady.connect(self.onBufferReady)
+ self._decoder.finished.connect(self.decoderFinished)
+
+ def file(self) -> QUrl | None:
+ return self._file_url
+
+ def setFile(self, value: QUrl):
+ if self._decoder.source() == value:
+ return
+
+ if self._audio_file and self._audio_file.isOpen():
+ self._audio_file.close()
+
+ self._waveformData = []
+ self._decoder.stop()
+
+ self._file_url = value
+ if "__compiled__" in globals():
+ path = self._file_url.toString().replace("qrc:/", ":/")
+ else:
+ path = self._file_url.path()
+ self._audio_file = QFile(path)
+ self._audio_file.open(QFile.ReadOnly)
+ self._decoder.setSourceDevice(self._audio_file)
+ self._decoder.start()
+ self.fileChanged.emit()
+
+ def paint(self, painter):
+ # Fill the bounding rectangle with the specified color
+ painter.fillRect(self.boundingRect(), self._background_color)
+
+ # If no waveform data is available, draw the text
+ if not self._waveformData:
+ painter.setPen(Qt.white)
+ painter.drawText(self.boundingRect(), Qt.AlignCenter, "Waveform not available")
+ return
+
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ # Set the pen for drawing the waveform
+ pen = QPen(Qt.blue)
+ pen.setWidth(1)
+ painter.setPen(pen)
+
+ # Get container dimensions
+ rect = self.boundingRect()
+ data_size = len(self._waveformData)
+
+ # Calculate step size and center line
+ x_step = rect.width() / data_size
+ center_y = rect.height() / 2.0
+
+ # Draw the waveform as connected lines
+ for i in range(1, data_size):
+ x1 = (i - 1) * x_step
+ y1 = center_y - self._waveformData[i - 1] * center_y
+ x2 = i * x_step
+ y2 = center_y - self._waveformData[i] * center_y
+ painter.drawLine(QPointF(x1, y1), QPointF(x2, y2))
+
+ @staticmethod
+ def float_buffer_to_list(data):
+ # Calculate the number of 32-bit floats in the buffer
+ float_count = len(data) // 4 # Each float32 is 4 bytes
+ # Unpack the binary data into a list of floats
+ return list(struct.unpack(f"{float_count}f", data))
+
+ def onBufferReady(self):
+ buffer = self._decoder.read()
+ data = buffer.constData()
+ self._waveformData.extend(self.float_buffer_to_list(data))
+ self.update()
+
+ file: QUrl = Property(QUrl, file, setFile, notify=fileChanged)
+
+ def decoderFinished(self):
+ self._audio_file.close()
diff --git a/examples/tutorials/drumpad/final_project/Python/autogen/settings.py b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py
new file mode 100644
index 000000000..39386a277
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/autogen/settings.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+# This file is automatically generated by Qt Design Studio.
+import os
+import sys
+from pathlib import Path
+
+from PySide6.QtQml import QQmlApplicationEngine
+
+project_root = Path(__file__).parent.parent.parent
+
+
+def setup_qt_environment(qml_engine: QQmlApplicationEngine):
+ """
+ Load the QML application. Import the compiled resources when the application is deployed.
+ """
+ qml_app_url = "DrumpadContent/App.qml"
+
+ if "__compiled__" in globals():
+ # Application has been deployed using pyside6-deploy
+ try:
+ import autogen.resources # noqa: F401
+ except ImportError:
+ resource_file = Path(__file__).parent / "resources.py"
+ print(
+ f"Error: No compiled resources found in {resource_file.absolute()}\n"
+ f"Please compile the resources using pyside6-rcc or pyside6-project build",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ qml_engine.addImportPath(":/")
+ qml_engine.load(f":/{qml_app_url}")
+ return
+
+ qml_engine.addImportPath(str(project_root.absolute()))
+ os.environ["QT_QUICK_CONTROLS_CONF"] = str(project_root / "qtquickcontrols2.conf")
+ qml_engine.load(str(project_root / qml_app_url))
diff --git a/examples/tutorials/drumpad/final_project/Python/main.py b/examples/tutorials/drumpad/final_project/Python/main.py
new file mode 100644
index 000000000..166f25144
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/main.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import sys
+
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQml import QQmlApplicationEngine
+
+from autogen.settings import setup_qt_environment
+from audio import * # noqa: F401,F403
+
+
+def main():
+ app = QGuiApplication(sys.argv)
+ engine = QQmlApplicationEngine()
+
+ setup_qt_environment(engine)
+
+ if not engine.rootObjects():
+ sys.exit(-1)
+
+ ex = app.exec()
+ del engine
+ return ex
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/examples/tutorials/drumpad/final_project/Python/pyproject.toml b/examples/tutorials/drumpad/final_project/Python/pyproject.toml
new file mode 100644
index 000000000..fcb5bbb9e
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/Python/pyproject.toml
@@ -0,0 +1,5 @@
+[project]
+name = "Drumpad"
+
+[tool.pyside6-project]
+files = ["main.py", "autogen/settings.py", "audio/audio_files_model.py", "audio/audio_engine.py", "audio/waveform_item.py", "../Drumpad.qmlproject", "../Drumpad.qrc", "../qtquickcontrols2.conf", "../Drumpad/AvailableSoundsComboBox.qml", "../Drumpad/CenteredFlow.qml", "../Drumpad/Constants.qml", "../Drumpad/PadButton.qml", "../Drumpad/qmldir", "../Drumpad/SoundEffectPlayer.qml", "../Drumpad/StyledSpinBox.qml", "../Drumpad/VolumeSlider.qml", "../DrumpadContent/App.qml", "../DrumpadContent/MainScreen.qml", "../DrumpadContent/qmldir", "../Mocks/Audio/AudioEngine.qml", "../Mocks/Audio/qmldir", "../Mocks/Audio/WaveformItem.qml", "../Mocks/Components/AudioFilesModel.qml", "../Mocks/Components/qmldir"]
diff --git a/examples/tutorials/drumpad/final_project/doc/final_project.md b/examples/tutorials/drumpad/final_project/doc/final_project.md
new file mode 100644
index 000000000..877bc0176
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/doc/final_project.md
@@ -0,0 +1,12 @@
+# Drumpad example (Qt Design Studio) - Final project
+
+This example contains the final [Qt Design Studio] project of the [Qt Design Studio integration tutorial].
+It contains all the necessary files to execute the project, including the Python code developed
+along the tutorial.
+
+For more details, see the [Qt Design Studio integration tutorial].
+
+To download the initial project source code, visit {ref}`example_tutorials_drumpad_initial_project`.
+
+[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/
+[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration
diff --git a/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf
new file mode 100644
index 000000000..87a95d011
--- /dev/null
+++ b/examples/tutorials/drumpad/final_project/qtquickcontrols2.conf
@@ -0,0 +1,6 @@
+; This file can be edited to change the style of the application
+; Read "Qt Quick Controls 2 Configuration File" for details:
+; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html
+
+[Controls]
+Style=Basic
diff --git a/examples/tutorials/drumpad/initial_project/.gitignore b/examples/tutorials/drumpad/initial_project/.gitignore
new file mode 100644
index 000000000..855f31da5
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/.gitignore
@@ -0,0 +1,11 @@
+__pycache__/
+.DS_Store
+build/
+deployment/
+pysidedeploy.spec
+resources.py
+*.autosave
+*.dist/
+Dependencies/
+*.qtds
+.qmlls.ini
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject
new file mode 100644
index 000000000..5e5414ae2
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad.qmlproject
@@ -0,0 +1,69 @@
+// prop: json-converted
+// prop: auto-generated
+
+import QmlProject
+
+Project {
+ mainFile: "DrumpadContent/App.qml"
+ mainUiFile: "DrumpadContent/MainScreen.qml"
+ targetDirectory: "/opt/Drumpad"
+ enableCMakeGeneration: false
+ enablePythonGeneration: false
+ widgetApp: true
+ importPaths: [ "." ]
+ mockImports: [ "Mocks" ]
+
+ qdsVersion: "4.5"
+ quickVersion: "6.7"
+ qt6Project: true
+ qtForMCUs: false
+
+ multilanguageSupport: true
+ primaryLanguage: "en"
+ supportedLanguages: [ "en" ]
+
+ Environment {
+ QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1"
+ QT_AUTO_SCREEN_SCALE_FACTOR: "1"
+ QT_ENABLE_HIGHDPI_SCALING: "0"
+ QT_LOGGING_RULES: "qt.qml.connections=false"
+ QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf"
+ }
+
+ QmlFiles {
+ directory: "Drumpad"
+ }
+
+ QmlFiles {
+ directory: "DrumpadContent"
+ }
+
+ QmlFiles {
+ directory: "Generated"
+ }
+
+ Files {
+ directory: "../Sounds"
+ filter: "*.mp3;*.wav"
+ }
+
+ QmlFiles {
+ directory: "Mocks/Audio"
+ }
+
+ Files {
+ files: [
+ "qtquickcontrols2.conf"
+ ]
+ }
+
+ Files {
+ directory: "Drumpad"
+ filter: "qmldir"
+ }
+
+ Files {
+ directory: "DrumpadContent"
+ filter: "*.ttf;*.otf"
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad.qrc b/examples/tutorials/drumpad/initial_project/Drumpad.qrc
new file mode 100644
index 000000000..b856a46a9
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad.qrc
@@ -0,0 +1,23 @@
+<RCC>
+ <qresource>
+ <file>Drumpad.qmlproject</file>
+ <file>Drumpad/AvailableSoundsComboBox.qml</file>
+ <file>Drumpad/CenteredFlow.qml</file>
+ <file>Drumpad/Constants.qml</file>
+ <file>Drumpad/PadButton.qml</file>
+ <file>Drumpad/qmldir</file>
+ <file>Drumpad/SoundEffectPlayer.qml</file>
+ <file>Drumpad/StyledSpinBox.qml</file>
+ <file>Drumpad/VolumeSlider.qml</file>
+ <file>DrumpadContent/App.qml</file>
+ <file>DrumpadContent/MainScreen.qml</file>
+ <file>DrumpadContent/qmldir</file>
+ <file>qtquickcontrols2.conf</file>
+ <file>../Sounds/Bongo Loop 125bpm.wav</file>
+ <file>../Sounds/Clap.wav</file>
+ <file>../Sounds/Closed Hat.wav</file>
+ <file>../Sounds/Kick Drum.wav</file>
+ <file>../Sounds/Open Hat.wav</file>
+ <file>../Sounds/Sine Bass Ebm.wav</file>
+ </qresource>
+</RCC>
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml
new file mode 100644
index 000000000..e105e2226
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/AvailableSoundsComboBox.qml
@@ -0,0 +1,111 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtQuick.Controls
+import Audio
+
+ComboBox {
+ id: root
+
+ property string currentFile: currentText ? `../Sounds/${currentText}` : ""
+ required property int initialIndex
+
+ model: audioFilesModel.getModel()
+
+ background: Rectangle {
+ border.color: root.pressed ? Constants.primaryColor : Constants.secondaryColor
+ border.width: root.visualFocus ? 3 : 2
+ color: root.pressed ? Constants.secondaryColor : "black"
+ implicitHeight: 30
+ radius: 2
+ }
+ contentItem: Text {
+ color: "white"
+ elide: Text.ElideRight
+ leftPadding: 10
+ rightPadding: root.indicator.width + 10
+ text: root.displayText
+ verticalAlignment: Text.AlignVCenter
+ }
+ delegate: ItemDelegate {
+ id: delegate
+
+ required property int index
+
+ highlighted: root.highlightedIndex === index
+
+ background: Rectangle {
+ color: delegate.highlighted ? Constants.darkGray : "black"
+ implicitWidth: delegate.contentItem.implicitWidth
+ width: popup.width
+ }
+ contentItem: Text {
+ anchors.fill: parent
+ color: delegate.highlighted ? "#ff0000" : "white"
+ elide: Text.ElideRight
+ leftPadding: 10
+ text: root.model[delegate.index]
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ indicator: Canvas {
+ id: canvas
+
+ contextType: "2d"
+ height: 8
+ width: 12
+ x: root.width - canvas.width - root.rightPadding
+ y: root.topPadding + (root.availableHeight - canvas.height) / 2
+
+ onPaint: {
+ let margin = 2;
+ context.reset();
+ context.lineWidth = 2;
+ context.strokeStyle = "white";
+ context.lineCap = "round";
+ context.beginPath();
+ context.moveTo(margin, margin);
+ context.lineTo(width / 2, height - margin);
+ context.lineTo(width - margin, margin);
+ context.stroke();
+ }
+
+ Connections {
+ function onPressedChanged() {
+ canvas.requestPaint();
+ }
+
+ target: root
+ }
+ }
+ popup: Popup {
+ id: popup
+
+ implicitHeight: contentItem.implicitHeight
+ implicitWidth: 200
+ padding: 2
+ y: root.height + 2
+
+ background: Rectangle {
+ border.color: Constants.primaryColor
+ border.width: 2
+ color: "black"
+ }
+ contentItem: ListView {
+ clip: true
+ currentIndex: root.highlightedIndex
+ implicitHeight: Math.min(contentHeight, 200)
+ model: popup.visible ? root.delegateModel : null
+ }
+ }
+
+ Component.onCompleted: {
+ currentIndex = root.initialIndex % model.length;
+ }
+
+ AudioFilesModel {
+ id: audioFilesModel
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml
new file mode 100644
index 000000000..a5e9fe2cc
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/CenteredFlow.qml
@@ -0,0 +1,22 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+
+// A Flow layout that centers its children horizontally
+// Note that the implementation adds unnecessary spacing in rows that are not full
+Flow {
+ property int customMargin: (children.length && (children[0].width + spacing <= parentWidth))
+ ? (parentWidth - rowWidth) / 2 + padding
+ : padding
+ property int parentWidth: parent.width - 2 * padding
+ property int rowCount: children.length ? parentWidth / (children[0].width + spacing) : 0
+ property int rowWidth: children.length
+ ? rowCount * children[0].width + (rowCount - 1) * spacing + 2 * padding
+ : 0
+
+ anchors {
+ leftMargin: customMargin
+ rightMargin: customMargin
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml
new file mode 100644
index 000000000..73058229c
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/Constants.qml
@@ -0,0 +1,12 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+pragma Singleton
+import QtQuick
+
+QtObject {
+ readonly property string darkGray: "#333333"
+ readonly property string mediumGray: "#9B9B9B"
+ readonly property string primaryColor: "#FF0000"
+ readonly property string secondaryColor: "#8C0000"
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml
new file mode 100644
index 000000000..e00d77db6
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/PadButton.qml
@@ -0,0 +1,110 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Shapes
+
+Rectangle {
+ id: root
+
+ property bool isPlaying: false
+ property bool isError: false
+ property bool isLoading: false
+ property int cornerRadius: 10
+ signal pressed()
+
+ color: "transparent"
+
+ Shape {
+ anchors.fill: parent
+
+ ShapePath {
+ strokeColor: "black"
+ strokeWidth: 2
+
+ fillGradient: RadialGradient {
+ centerRadius: root.height
+ centerX: root.width / 2
+ centerY: root.height / 2
+ focalX: centerX
+ focalY: centerY
+
+ GradientStop {
+ position: 0
+ color: {
+ if (isError)
+ return "black";
+ if (isLoading)
+ return "yellow";
+ if (isPlaying)
+ return Qt.darker(Constants.primaryColor, 1.25);
+ return Qt.darker(Constants.secondaryColor, 1.25);
+ }
+ }
+ GradientStop {
+ position: 0.5
+ color: {
+ if (isError)
+ return Constants.darkGray;
+ if (isLoading)
+ return "orange";
+ if (isPlaying)
+ return Constants.primaryColor;
+ return Constants.secondaryColor;
+ }
+ }
+ }
+
+ // Rounded shape path
+ PathMove {
+ x: root.cornerRadius
+ y: 0
+ }
+ PathQuad {
+ controlX: 0
+ controlY: 0
+ x: 0
+ y: root.cornerRadius
+ }
+ PathLine {
+ x: 0
+ y: root.height - root.cornerRadius
+ }
+ PathQuad {
+ controlX: 0
+ controlY: root.height
+ x: root.cornerRadius
+ y: root.height
+ }
+ PathLine {
+ x: root.width - root.cornerRadius
+ y: root.height
+ }
+ PathQuad {
+ controlX: root.width
+ controlY: root.height
+ x: root.width
+ y: root.height - root.cornerRadius
+ }
+ PathLine {
+ x: root.width
+ y: root.cornerRadius
+ }
+ PathQuad {
+ controlX: root.width
+ controlY: 0
+ x: root.width - root.cornerRadius
+ y: 0
+ }
+ PathLine {
+ x: root.cornerRadius
+ y: 0
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.pressed()
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml
new file mode 100644
index 000000000..7232b9667
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/SoundEffectPlayer.qml
@@ -0,0 +1,118 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import QtMultimedia
+
+import Drumpad
+import Audio
+
+Rectangle {
+ id: root
+
+ property string decodingError: ""
+ required property int index
+ property int status: SoundEffect.Null
+ property bool isLoading: status == SoundEffect.Loading
+ property bool isError: status == SoundEffect.Error || status == SoundEffect.Null
+ property bool isReady: status == SoundEffect.Ready
+
+ function play() {
+ if (root.status == SoundEffect.Ready) {
+ audioEngine.play();
+ }
+ }
+
+ color: Constants.darkGray
+ implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins
+ implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins
+ radius: 10
+
+ onDecodingErrorChanged: {
+ if (status == SoundEffect.Error && root.decodingError) {
+ errorMessageDialog.text = root.decodingError;
+ errorMessageDialog.open();
+ }
+ }
+
+ AudioEngine {
+ id: audioEngine
+
+ file: availableSoundsComboBox.currentFile
+ volume: volumeSlider.value
+
+ onDecodingStatusChanged: (status, error) => {
+ root.status = status;
+ if (status == SoundEffect.Error && error) {
+ root.decodingError = error;
+ } else {
+ root.decodingError = "";
+ }
+ }
+ }
+
+ MessageDialog {
+ id: errorMessageDialog
+
+ buttons: MessageDialog.Ok
+ title: "Error decoding file"
+ }
+
+ ColumnLayout {
+ id: layout
+
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 10
+
+ RowLayout {
+ spacing: 10
+
+ Text {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ color: "white"
+ text: `Player ${root.index + 1}`
+ }
+ AvailableSoundsComboBox {
+ id: availableSoundsComboBox
+
+ Layout.alignment: Qt.AlignCenter
+ initialIndex: root.index
+ }
+ }
+
+ WaveformItem {
+ id: waveformItem
+
+ file: audioEngine.file
+ height: 100
+ width: 300
+ }
+
+ Row {
+ Layout.alignment: Qt.AlignCenter
+ spacing: 10
+
+ PadButton {
+ id: padRectangle
+ height: 100
+ width: 100
+ isPlaying: audioEngine.isPlaying
+ isError: root.isError
+ isLoading: root.isLoading
+ onPressed: root.play()
+ }
+
+ VolumeSlider {
+ id: volumeSlider
+
+ height: padRectangle.height
+ value: 0.75
+ width: 16
+ }
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml
new file mode 100644
index 000000000..c403be0d8
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/StyledSpinBox.qml
@@ -0,0 +1,68 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+
+SpinBox {
+ id: root
+
+ property int innerPadding: 10
+
+ height: contentItem.implicitHeight + innerPadding
+ width: contentItem.width + up.indicator.implicitWidth + down.indicator.implicitWidth
+
+ background: Rectangle {
+ border.color: Constants.secondaryColor
+ }
+
+ contentItem: Text {
+ color: "black"
+ height: parent.height
+ horizontalAlignment: Text.AlignHCenter
+ text: root.textFromValue(root.value, root.locale)
+ verticalAlignment: Text.AlignVCenter
+ width: implicitWidth + innerPadding * 2
+ }
+
+ down.indicator: Rectangle {
+ border.color: Constants.secondaryColor
+ color: root.down.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black"
+ height: parent.height
+ implicitWidth: downText.implicitWidth + innerPadding * 2
+ x: root.mirrored ? parent.width - width : 0
+
+ Text {
+ id: downText
+
+ anchors.fill: parent
+ color: "white"
+ font.pixelSize: Math.round(root.font.pixelSize * 1.5)
+ fontSizeMode: Text.Fit
+ horizontalAlignment: Text.AlignHCenter
+ text: "-"
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ up.indicator: Rectangle {
+ border.color: Constants.secondaryColor
+ color: root.up.pressed ? Constants.mediumGray : enabled ? Constants.darkGray : "black"
+ height: parent.height
+ implicitWidth: upText.implicitWidth + innerPadding * 2
+ x: root.mirrored ? 0 : parent.width - width
+
+ Text {
+ id: upText
+
+ anchors.centerIn: parent
+ anchors.fill: parent
+ color: "white"
+ font.pixelSize: Math.round(root.font.pixelSize * 1.5)
+ fontSizeMode: Text.Fit
+ horizontalAlignment: Text.AlignHCenter
+ text: "+"
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml
new file mode 100644
index 000000000..0fd1eea45
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/VolumeSlider.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+
+Slider {
+ id: root
+
+ orientation: Qt.Vertical
+ padding: 0
+
+ background: Rectangle {
+ color: Constants.mediumGray
+ implicitHeight: root.height
+ implicitWidth: root.width
+ radius: width / 2
+
+ Rectangle {
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: Qt.lighter(Constants.primaryColor, 1 - (root.visualPosition * 0.3))
+ height: (1 - root.visualPosition) * parent.height + (root.visualPosition * handle.height)
+ radius: parent.width / 2
+ width: parent.width
+ }
+ }
+
+ handle: Rectangle {
+ border.color: "#b0b0b0"
+ border.width: 1
+ color: root.pressed ? "#e0e0e0" : "#ffffff"
+ height: root.width
+ radius: width / 2
+ width: root.width
+ x: root.availableWidth / 2 - height / 2
+ y: root.visualPosition * (root.availableHeight - height)
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Drumpad/qmldir b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir
new file mode 100644
index 000000000..7dba78c17
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Drumpad/qmldir
@@ -0,0 +1,10 @@
+module Drumpad
+
+AvailableSoundsComboBox 1.0 AvailableSoundsComboBox.qml
+SoundEffectPlayer 1.0 SoundEffectPlayer.qml
+CenteredFlow 1.0 CenteredFlow.qml
+VolumeSlider 1.0 VolumeSlider.qml
+StyledSpinBox 1.0 StyledSpinBox.qml
+PadButton 1.0 PadButton.qml
+
+singleton Constants 1.0 Constants.qml
diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml
new file mode 100644
index 000000000..e1e4b4d3c
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/App.qml
@@ -0,0 +1,21 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import Drumpad 1.0
+
+Window {
+ id: root
+
+ height: 800
+ title: "Drumpad"
+ visible: true
+ width: 1200
+
+ MainScreen {
+ id: mainScreen
+
+ anchors.fill: parent
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml
new file mode 100644
index 000000000..2754c4c93
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/MainScreen.qml
@@ -0,0 +1,99 @@
+// 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
+import Drumpad
+import Audio
+
+Rectangle {
+ id: root
+
+ property QtObject soundEffectPlayer: Qt.createComponent("../Drumpad/SoundEffectPlayer.qml",
+ Component.PreferSynchronous)
+
+ color: "black"
+ focus: true
+
+ Component.onCompleted: {
+ // Initialize the default sound effect players
+ for (var i = 0; i < audioPlayersSpinBox.value; i++) {
+ root.soundEffectPlayer.createObject(soundEffectPlayersFlow, {
+ index: i
+ });
+ }
+ }
+ Keys.onPressed: event => {
+ if (event.key < Qt.Key_1 || event.key > Qt.Key_9) {
+ // Ignore key out of scope
+ return;
+ }
+
+ let digit = event.key - Qt.Key_1;
+ if (digit < soundEffectPlayersFlow.children.length) {
+ soundEffectPlayersFlow.children[digit].play();
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Row {
+ id: audioPlayersCountRow
+
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 5
+
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ color: "white"
+ text: "Audio players:"
+ }
+
+ StyledSpinBox {
+ id: audioPlayersSpinBox
+
+ value: 5
+
+ onValueModified: {
+ let soundPlayersCount = soundEffectPlayersFlow.children.length;
+ if (audioPlayersSpinBox.value < soundPlayersCount) {
+ // Remove extra sound effect players
+ soundEffectPlayersFlow.children.length = audioPlayersSpinBox.value;
+ return;
+ }
+
+ if (audioPlayersSpinBox.value < soundPlayersCount) {
+ return;
+ }
+ // Create more sound effect players
+ for (var i = soundPlayersCount; i < audioPlayersSpinBox.value; i++) {
+ root.soundEffectPlayer.createObject(soundEffectPlayersFlow, {
+ index: i
+ });
+ }
+ }
+ }
+ }
+
+ ScrollView {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ contentWidth: width
+
+ background: Rectangle {
+ color: "#232323"
+ }
+
+ CenteredFlow {
+ id: soundEffectPlayersFlow
+
+ anchors.fill: parent
+ padding: 10
+ spacing: 10
+ }
+ }
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir
new file mode 100644
index 000000000..f1f34c528
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/DrumpadContent/qmldir
@@ -0,0 +1,4 @@
+module DrumpadContent
+
+App 1.0 App.qml
+MainScreen 1.0 MainScreen.qml
diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml
new file mode 100644
index 000000000..4bfbc24f3
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/AudioEngine.qml
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtMultimedia
+
+Item {
+ id: root
+
+ property double volume
+ property url file
+
+ MediaPlayer {
+ id: player
+ source: file
+ audioOutput: AudioOutput {}
+ }
+
+ onVolumeChanged : {
+ console.log("Mock: VolumeChanaged ", volume )
+ }
+
+ function play() {
+ console.log("Mock: play()")
+ player.play()
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml
new file mode 100644
index 000000000..fcb6041bb
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/WaveformItem.qml
@@ -0,0 +1,13 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ id: root
+ width: 1920
+ height: 1080
+ color: "blue"
+ property url file
+}
diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir
new file mode 100644
index 000000000..189d68e6b
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Mocks/Audio/qmldir
@@ -0,0 +1,3 @@
+module Audio
+AudioEngine 1.0 AudioEngine.qml
+WaveformItem 1.0 WaveformItem.qml
diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml
new file mode 100644
index 000000000..b06a1e17a
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/AudioFilesModel.qml
@@ -0,0 +1,8 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+Item {
+ getFiles: function() {
+ console.log("AudioFilesModel mock: getFiles()")
+ }
+}
diff --git a/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir
new file mode 100644
index 000000000..2d8e98995
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/Mocks/Components/qmldir
@@ -0,0 +1,2 @@
+module Components
+AudioFilesModel 1.0 AudioFilesModel.qml
diff --git a/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject
new file mode 100644
index 000000000..1841cd88e
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/doc/drumpad_initial_project.pyproject
@@ -0,0 +1,21 @@
+{
+ "files": ["../Drumpad.qmlproject",
+ "../Drumpad.qrc",
+ "../qtquickcontrols2.conf",
+ "../Drumpad/AvailableSoundsComboBox.qml",
+ "../Drumpad/CenteredFlow.qml",
+ "../Drumpad/Constants.qml",
+ "../Drumpad/PadButton.qml",
+ "../Drumpad/qmldir",
+ "../Drumpad/SoundEffectPlayer.qml",
+ "../Drumpad/StyledSpinBox.qml",
+ "../Drumpad/VolumeSlider.qml",
+ "../DrumpadContent/App.qml",
+ "../DrumpadContent/MainScreen.qml",
+ "../DrumpadContent/qmldir",
+ "../Mocks/Audio/AudioEngine.qml",
+ "../Mocks/Audio/qmldir",
+ "../Mocks/Audio/WaveformItem.qml",
+ "../Mocks/Components/AudioFilesModel.qml",
+ "../Mocks/Components/qmldir"]
+}
diff --git a/examples/tutorials/drumpad/initial_project/doc/initial_project.md b/examples/tutorials/drumpad/initial_project/doc/initial_project.md
new file mode 100644
index 000000000..b874d85e2
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/doc/initial_project.md
@@ -0,0 +1,12 @@
+# Drumpad example (Qt Design Studio) - Initial project
+
+This example contains the initial [Qt Design Studio] project to be used as a starting point for the
+[Qt Design Studio integration tutorial]. **It is not an executable project as is**, since it does
+**not** contain the required Python code developed along the tutorial.
+
+For more details, see the [Qt Design Studio integration tutorial].
+
+To download the final project source code, visit {ref}`example_tutorials_drumpad_final_project`.
+
+[Qt Design Studio]: https://www.qt.io/product/ui-design-tools/
+[Qt Design Studio integration tutorial]: tutorial_qt_design_studio_integration
diff --git a/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf
new file mode 100644
index 000000000..87a95d011
--- /dev/null
+++ b/examples/tutorials/drumpad/initial_project/qtquickcontrols2.conf
@@ -0,0 +1,6 @@
+; This file can be edited to change the style of the application
+; Read "Qt Quick Controls 2 Configuration File" for details:
+; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html
+
+[Controls]
+Style=Basic