Qt Design Studio integration tutorial

Summary

This tutorial provides a step-by-step guide for exporting a Qt Design Studio project for Python development and deployment. You will learn how to:

  • Export a Qt Design Studio in order to get a project template for further Python development

  • Implement custom QML elements using Python

  • Successfully deploy the PySide6 application

Note

This tutorial is not focused on teaching how to use Qt Design Studio or QML, but rather how to integrate PySide6 with an existing Qt Design Studio project. If you want to learn how to use Qt Design Studio, check the available tutorials.

The project consists in a single “drumpad” screen that can be used to play different sound effects. The screen is composed of a responsive grid of buttons, each playing a different sound. In addition, a waveform display shows the audio amplitude over time using Qt Multimedia features.

Drumpad example screenshot

Workflow overview

Before starting the tutorial, we need to understand the Qt Design Studio project workflow first.

  1. Create a QML project using Qt Design Studio: Develop the application UI in a user friendly way. You can visually design components, screens and animations without writing QML code manually.

  2. Export the project: Create a Python project using the Qt Design Studio generator.

  3. Develop logic: Implement custom functionalities and application logic in Python, connecting it to the exported QML files. Define backend elements and signal communication with the UI.

  4. Deploy: Package the application into a standalone executable using the pyside6-deploy tool. This bundles all required dependencies, resources, and modules into a distributable format.

Qt Design Studio project set up

The initial project source code is available for download at Drumpad example (Qt Design Studio) - Initial project. This provides the starting point for the tutorial and includes a set of QML files, Qt Resource files, and other project files.

Qt Design Studio showing the main screen

Qt Design Studio offers a Python project template generator. The option can be enabled in the File > Export project > Enable Python Generator setting.

Qt Design Studio Enable Python Generator setting

When the setting is enabled, Qt Design Studio will create a Python folder in the project directory, containing the main.py and pyproject.toml files as well as the autogen folder. The autogen folder contains the settings.py file, which is used to set up the project root path, the QML import paths and other Qt specific settings.

Python development

The project contains three Python files that define QML elements located in the Python/audio folder. They belong to the Audio QML module. The QML code expects that they exist. Otherwise, the application can not be executed.

The AudioEngine QML element is responsible for playing audio files. It uses the QSoundEffect class from the Qt Multimedia module to play the audio files. It also provides Qt Signals for communicating with the QML layer.

audio_engine.py
audio_engine.py
 1# Copyright (C) 2025 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4from PySide6.QtQml import QmlElement
 5from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl
 6from PySide6.QtMultimedia import QSoundEffect
 7
 8from autogen.settings import project_root
 9
10QML_IMPORT_NAME = "Audio"
11QML_IMPORT_MAJOR_VERSION = 1
12
13
14@QmlElement
15class AudioEngine(QObject):
16    volumeChanged = Signal()
17    fileChanged = Signal()
18    isPlayingChanged = Signal()
19    decodingStatusChanged = Signal(QSoundEffect.Status, str)
20
21    def __init__(self, parent=None):
22        super().__init__(parent)
23        self._sound_effect = QSoundEffect()
24        self._sound_effect.playingChanged.connect(self.isPlayingChanged.emit)  #
25        self._sound_effect.statusChanged.connect(self.reportStatus)
26
27    def reportStatus(self):
28        if self._sound_effect.status() == QSoundEffect.Status.Error:
29            self.decodingStatusChanged.emit(
30                QSoundEffect.Status.Error,
31                f"Error decoding file: {self._sound_effect.source().path()}",
32            )
33        else:
34            self.decodingStatusChanged.emit(self._sound_effect.status(), "")
35
36    @Slot(result=None)
37    def play(self):
38        self._sound_effect.play()
39
40    def volume(self):
41        return self._sound_effect.volume()
42
43    def setVolume(self, value):
44        self._sound_effect.setVolume(value)
45        self.volumeChanged.emit()
46
47    def file(self):
48        return self._sound_effect.source()
49
50    def setFile(self, value: QUrl):
51        if self._sound_effect.source() == value or value.isEmpty():
52            return
53
54        if "__compiled__" in globals():
55            self._sound_effect.setSource(f"qrc:/{value.toString()}")
56        else:
57            self._sound_effect.setSource(f"file:{project_root / value.toString()}")
58        self.fileChanged.emit()
59
60    def isPlaying(self):
61        return self._sound_effect.isPlaying()
62
63    volume = Property(float, volume, setVolume, notify=volumeChanged)
64    file = Property(QUrl, file, setFile, notify=fileChanged)
65    isPlaying = Property(bool, isPlaying, notify=isPlayingChanged)

The AudioFilesModel QML element is responsible for managing the audio files. It fetches the available audio files from the Sounds folder and provides a getModel() method to return a list of files. It detects whether the application has been deployed because the compiled Qt resource files are used in this case.

audio_files_model.py
audio_files_model.py
 1# Copyright (C) 2025 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4from pathlib import Path
 5
 6from PySide6.QtCore import QObject, Slot, QDirIterator
 7from PySide6.QtQml import QmlElement
 8
 9from autogen.settings import project_root
10
11
12QML_IMPORT_NAME = "Audio"
13QML_IMPORT_MAJOR_VERSION = 1
14
15
16@QmlElement
17class AudioFilesModel(QObject):
18    @Slot(result=list)
19    def getModel(self):
20        if "__compiled__" in globals():
21            resource_prefix = ":/Sounds/"
22            iterator = QDirIterator(resource_prefix, QDirIterator.Subdirectories)
23            audio_files = []
24            while iterator.hasNext():
25                resource = iterator.next()
26                audio_files.append(resource.split(resource_prefix)[-1])
27            return audio_files
28
29        return list(p.name for p in Path(project_root / "Sounds").glob("*.wav"))

The WaveformItem QML element is responsible for displaying the audio waveform. It uses the QAudioDecoder and QAudioFormat classes from the Qt Multimedia module to decode the audio file and display the waveform. The graph is drawn using QPainter.

waveform_item.py
waveform_item.py
  1# Copyright (C) 2025 The Qt Company Ltd.
  2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
  3
  4import struct
  5
  6from PySide6.QtCore import Qt, Property, QUrl, Signal, QFile, QPointF
  7from PySide6.QtGui import QPen, QPainter
  8from PySide6.QtMultimedia import QAudioFormat, QAudioDecoder
  9from PySide6.QtQml import QmlElement
 10from PySide6.QtQuick import QQuickPaintedItem
 11
 12QML_IMPORT_NAME = "Audio"
 13QML_IMPORT_MAJOR_VERSION = 1
 14
 15
 16@QmlElement
 17class WaveformItem(QQuickPaintedItem):
 18
 19    fileChanged = Signal()
 20
 21    def __init__(self, parent=None):
 22        super().__init__(parent)
 23        self._waveformData = []
 24        self._background_color = Qt.black
 25
 26        audio_format = QAudioFormat()
 27        audio_format.setChannelCount(1)
 28        audio_format.setSampleRate(44100)
 29        audio_format.setSampleFormat(QAudioFormat.Float)
 30
 31        self._file_url: QUrl | None = None
 32        self._audio_file: QFile | None = None
 33
 34        self._decoder = QAudioDecoder()
 35        self._decoder.setAudioFormat(audio_format)
 36
 37        self._decoder.bufferReady.connect(self.onBufferReady)
 38        self._decoder.finished.connect(self.decoderFinished)
 39
 40    def file(self) -> QUrl | None:
 41        return self._file_url
 42
 43    def setFile(self, value: QUrl):
 44        if self._decoder.source() == value:
 45            return
 46
 47        if self._audio_file and self._audio_file.isOpen():
 48            self._audio_file.close()
 49
 50        self._waveformData = []
 51        self._decoder.stop()
 52
 53        self._file_url = value
 54        if "__compiled__" in globals():
 55            path = self._file_url.toString().replace("qrc:/", ":/")
 56        else:
 57            path = self._file_url.path()
 58        self._audio_file = QFile(path)
 59        self._audio_file.open(QFile.ReadOnly)
 60        self._decoder.setSourceDevice(self._audio_file)
 61        self._decoder.start()
 62        self.fileChanged.emit()
 63
 64    def paint(self, painter):
 65        # Fill the bounding rectangle with the specified color
 66        painter.fillRect(self.boundingRect(), self._background_color)
 67
 68        # If no waveform data is available, draw the text
 69        if not self._waveformData:
 70            painter.setPen(Qt.white)
 71            painter.drawText(self.boundingRect(), Qt.AlignCenter, "Waveform not available")
 72            return
 73
 74        painter.setRenderHint(QPainter.Antialiasing)
 75
 76        # Set the pen for drawing the waveform
 77        pen = QPen(Qt.blue)
 78        pen.setWidth(1)
 79        painter.setPen(pen)
 80
 81        # Get container dimensions
 82        rect = self.boundingRect()
 83        data_size = len(self._waveformData)
 84
 85        # Calculate step size and center line
 86        x_step = rect.width() / data_size
 87        center_y = rect.height() / 2.0
 88
 89        # Draw the waveform as connected lines
 90        for i in range(1, data_size):
 91            x1 = (i - 1) * x_step
 92            y1 = center_y - self._waveformData[i - 1] * center_y
 93            x2 = i * x_step
 94            y2 = center_y - self._waveformData[i] * center_y
 95            painter.drawLine(QPointF(x1, y1), QPointF(x2, y2))
 96
 97    @staticmethod
 98    def float_buffer_to_list(data):
 99        # Calculate the number of 32-bit floats in the buffer
100        float_count = len(data) // 4  # Each float32 is 4 bytes
101        # Unpack the binary data into a list of floats
102        return list(struct.unpack(f"{float_count}f", data))
103
104    def onBufferReady(self):
105        buffer = self._decoder.read()
106        data = buffer.constData()
107        self._waveformData.extend(self.float_buffer_to_list(data))
108        self.update()
109
110    file: QUrl = Property(QUrl, file, setFile, notify=fileChanged)
111
112    def decoderFinished(self):
113        self._audio_file.close()

Running the application

Navigate to the Python/ directory of the project:

cd Python/

Then, build the project using:

pyside6-project build

This command will compile resources, UI files, QML files, and other necessary components.

Deployment

In order to create a standalone executable of the application, we can use the pyside6-deploy command line tool. It will analyze the project source code, determine the required Qt modules and dependencies and bundle the code into a native executable.

To deploy the application, execute the following command from the Python/ directory:

pyside6-deploy --name Drumpad

This will create a standalone executable for the application in the project directory.

Important

Make sure to fulfil the pyside6-deploy requirements for your platform. Otherwise, the tool will not detect that the example code uses Qt Multimedia module. In that case, the produced executable will not work properly.

Qt resource files

Note that since the main.py file is contained in the Python folder, its references to the project QML files and other resources have to traverse one level up. When the project is deployed, this is an issue because of the way Nuitka works. After the deployment, the main.py entry point file is morphed into a native executable file, but its location in the project folder changes:

Project structure before deployment:

├── Drumpad
│   ├── AvailableSoundsComboBox.qml
│   ...
├── Python
│   ├── main.py
│   ├── pyproject.toml
│   └── autogen
└── Sounds
    ├── Clap.wav
    ├── Kick Drum.wav
    ...

Project structure after deployment:

├── main.exe  (OS dependent executable format)
├── Drumpad
│   ├── AvailableSoundsComboBox.qml
│   ...
└── Sounds
    ├── Clap.wav
    ├── Kick Drum.wav
    ...

The relative location of the resources changes after the deployment. For example, before deploying the application, the path for accessing a sound from the main.py file would be: ../Sounds/Clap.wav. After the deployment, the relative path is now: Sounds/Clap.wav.

This issue is addressed by the pyside6-deploy tool thanks to the usage of Qt resource files. All the files listed in the Drumpad.qrc file are embedded in the executable and can be accessed by importing the Python/autogen/resources.py Python file. This way, the paths can be easily resolved properly after the deployment of the application.

Qt Design Studio creates the Python/autogen/settings.py file which contains code that enables the usage of the compiled Qt resources when the application is deployed. This code can be modified on demand.

Conclusion

In this tutorial, you learned how to integrate a user interface developed in Qt Design Studio with a Python backend using PySide6. We walked through the complete workflow, from exporting a QML project and implementing custom Python logic to packaging the application into a standalone executable using pyside6-deploy.