一键导入
pyqt
// PyQt/PySide6 overview hub - installation, comparison, project structure. See sub-skills for detailed topics.
// PyQt/PySide6 overview hub - installation, comparison, project structure. See sub-skills for detailed topics.
Build KDE Plasma 6 widgets with Python backend and QML UI, including metadata, deployment, and KDE Store distribution
Develop plugins, tools, and extensions for OpenCode AI coding agent with MCP, LSP integration, custom tools, and SDK usage
Async HTTP server and client for Python with WebSocket support, middleware, streaming, and server-sent events
Lua game framework for Gameboy Advance with sprites, tilemaps, entities, collision, audio, multiplayer
Data validation using Python type hints with Pydantic models, settings, serialization, and performance optimization
PyQt/PySide multimedia - audio playback, video playback, camera, audio recording, media player
| name | pyqt |
| description | PyQt/PySide6 overview hub - installation, comparison, project structure. See sub-skills for detailed topics. |
| metadata | {"author":"mte90","version":"2.0.0","tags":["python","qt","pyqt","pyside","gui","desktop","hub"]} |
PyQt and PySide are Python bindings for the Qt application framework for building cross-platform desktop applications.
For detailed information, see the specialized sub-skills:
| Skill | Description | Path |
|---|---|---|
| pyqt-core | Signals, slots, timers, settings, file I/O | core/SKILL.md |
| pyqt-widgets | All widgets and layouts | widgets/SKILL.md |
| pyqt-threading | QThread, thread pools, concurrency | threading/SKILL.md |
| pyqt-dialogs | Standard and custom dialogs | dialogs/SKILL.md |
| pyqt-testing | pytest-qt testing patterns | testing/SKILL.md |
| pyqt-styling | QSS styling and themes | styling/SKILL.md |
| pyqt-multimedia | Audio, video, camera, recording | multimedia/SKILL.md |
| Feature | PyQt5 | PyQt6 | PySide6 |
|---|---|---|---|
| License | GPL | GPL | LGPL |
| Qt Version | Qt 5 | Qt 6 | Qt 6 |
| Maintained | Security only | Active | Active |
| Signal Syntax | pyqtSignal | pyqtSignal | Signal |
| Slot Syntax | pyqtSlot | pyqtSlot | Slot |
| Property Syntax | pyqtProperty | pyqtProperty | Property |
| Commercial Use | Requires license | Requires license | Free |
| QML Registration | qmlRegisterType() | qmlRegisterType() | @QmlElement |
pip install PySide6
pip install PyQt6
pip install PyQt5
# System packages (Ubuntu/Debian)
sudo apt install libgl1-mesa-glx libglib2.0-0
# System packages (Fedora)
sudo dnf install mesa-libGL glib2
# System packages (Arch)
sudo pacman -S mesa glib2
#!/usr/bin/env python3
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My Application")
self.setGeometry(100, 100, 800, 600)
label = QLabel("Hello, Qt!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setCentralWidget(label)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
my_app/
├── src/
│ ├── __init__.py
│ ├── main.py
│ ├── main_window.py
│ ├── widgets/
│ │ ├── __init__.py
│ │ └── custom_widget.py
│ ├── models/
│ │ └── data_model.py
│ ├── resources/
│ │ ├── icons/
│ │ └── styles/
│ │ └── style.qss
│ └── utils/
│ └── helpers.py
├── tests/
│ └── test_main.py
├── requirements.txt
└── pyproject.toml
# QtWidgets - UI Components
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QLabel, QPushButton, QLineEdit, QTextEdit,
QComboBox, QSpinBox, QCheckBox, QRadioButton,
QSlider, QProgressBar, QGroupBox,
QTabWidget, QStackedWidget, QSplitter,
QListWidget, QTreeWidget, QTableWidget,
QScrollArea, QToolBar, QStatusBar,
QMenuBar, QMenu
)
# QtCore - Core Non-GUI
from PySide6.QtCore import (
Qt, QObject, QTimer, QThread,
Signal, Slot, Property,
QSize, QPoint, QRect,
QSettings, QFile, QDir,
QUrl, QMimeData,
QDateTime, QDate, QTime
)
# QtGui - Graphics
from PySide6.QtGui import (
QIcon, QPixmap, QImage,
QPainter, QPen, QBrush, QColor,
QFont, QCursor,
QKeySequence, QShortcut
)
from PySide6.QtCore import QObject, Signal, Slot
class MyObject(QObject):
valueChanged = Signal(int)
@Slot(int)
def setValue(self, value):
self._value = value
self.valueChanged.emit(value)
# Connect
button.clicked.connect(self.onButtonClick)
# Emit
self.valueChanged.emit(42)
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout
# Vertical
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
# Horizontal
h_layout = QHBoxLayout()
h_layout.addWidget(left)
h_layout.addWidget(right)
# Grid
grid = QGridLayout()
grid.addWidget(label, 0, 0)
grid.addWidget(input, 0, 1)
# Form
form = QFormLayout()
form.addRow("Name:", nameEdit)
from PySide6.QtCore import QObject, Signal
class MyObject(QObject):
# Define signals at class level
valueChanged = Signal(int)
nameChanged = Signal(str)
dataReady = Signal(dict)
errorOccurred = Signal(str)
# Signal with multiple arguments
positionChanged = Signal(int, int)
from PySide6.QtCore import Slot
class MyObject(QObject):
@Slot()
def doSomething(self):
print("Action performed")
@Slot(int)
def setValue(self, value):
self._value = value
@Slot(str, int)
def processData(self, name, count):
pass
@Slot(result=str) # Return type annotation
def getName(self) -> str:
return self._name
# Connect signal to slot
button.clicked.connect(self.onButtonClick)
valueChanged.connect(self.updateValue)
# Connect with lambda
button.clicked.connect(lambda: print("Clicked!"))
# Connect with partial
from functools import partial
button.clicked.connect(partial(self.processItem, item_id))
# Disconnect
button.clicked.disconnect(self.onButtonClick)
# Emit signal
self.valueChanged.emit(42)
self.positionChanged.emit(x, y)
# Block signals temporarily
button.blockSignals(True)
button.setChecked(True)
button.blockSignals(False)
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
class MyObject(QObject):
valueChanged = pyqtSignal(int)
@pyqtSlot(int)
def setValue(self, value):
pass
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QGroupBox
# Vertical layout
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
layout.addStretch() # Add stretchable space
layout.addWidget(bottom_label)
# Horizontal layout
h_layout = QHBoxLayout()
h_layout.addWidget(left_button)
h_layout.addStretch()
h_layout.addWidget(right_button)
# Nest layouts
main_layout = QVBoxLayout()
main_layout.addLayout(h_layout)
from PySide6.QtWidgets import QGridLayout
layout = QGridLayout()
layout.addWidget(label1, 0, 0) # row 0, col 0
layout.addWidget(lineEdit, 0, 1) # row 0, col 1
layout.addWidget(label2, 1, 0) # row 1, col 0
layout.addWidget(comboBox, 1, 1) # row 1, col 1
# Span multiple cells
layout.addWidget(bigWidget, 2, 0, 1, 2) # row 2, col 0, span 1 row, 2 cols
from PySide6.QtWidgets import QFormLayout
layout = QFormLayout()
layout.addRow("Name:", nameLineEdit)
layout.addRow("Email:", emailLineEdit)
layout.addRow("Age:", ageSpinBox)
from PySide6.QtWidgets import QStackedLayout
stack = QStackedLayout()
stack.addWidget(page1)
stack.addWidget(page2)
stack.addWidget(page3)
stack.setCurrentIndex(0) # Show page1
# Margins and spacing
layout.setContentsMargins(10, 10, 10, 10) # left, top, right, bottom
layout.setSpacing(5)
# Widget alignment
layout.addWidget(label, alignment=Qt.AlignCenter)
# Stretch factors
layout.addWidget(widget1, stretch=1)
layout.addWidget(widget2, stretch=2) # Gets twice the space
# Label
label = QLabel("Text")
label.setText("New text")
label.setPixmap(QPixmap("image.png"))
label.setAlignment(Qt.AlignCenter)
label.setWordWrap(True)
# Progress bar
progress = QProgressBar()
progress.setValue(50)
progress.setRange(0, 100)
progress.setTextVisible(True)
# LCD Number
lcd = QLCDNumber()
lcd.display(123)
# Line edit
lineEdit = QLineEdit()
lineEdit.setText("Default")
lineEdit.setPlaceholderText("Enter text...")
lineEdit.setEchoMode(QLineEdit.Password)
lineEdit.setMaxLength(100)
lineEdit.textChanged.connect(self.onTextChanged)
# Text edit
textEdit = QTextEdit()
textEdit.setPlainText("Plain text")
textEdit.setHtml("<b>HTML</b>")
textEdit.toPlainText()
# Spin box
spinBox = QSpinBox()
spinBox.setRange(0, 100)
spinBox.setValue(50)
spinBox.setSuffix(" px")
spinBox.valueChanged.connect(self.onValueChanged)
# Combo box
comboBox = QComboBox()
comboBox.addItems(["Option 1", "Option 2", "Option 3"])
comboBox.setCurrentIndex(0)
comboBox.currentTextChanged.connect(self.onSelectionChanged)
# Checkbox
checkBox = QCheckBox("Enable feature")
checkBox.setChecked(True)
checkBox.stateChanged.connect(self.onStateChanged)
# Radio button
radio1 = QRadioButton("Option A")
radio2 = QRadioButton("Option B")
radio1.setChecked(True)
radio1.toggled.connect(self.onToggled)
# Slider
slider = QSlider(Qt.Horizontal)
slider.setRange(0, 100)
slider.setValue(50)
slider.valueChanged.connect(self.onSliderChanged)
# Push button
button = QPushButton("Click Me")
button.clicked.connect(self.onButtonClick)
button.setEnabled(False)
button.setDefault(True)
# Tool button
toolButton = QToolButton()
toolButton.setIcon(QIcon("icon.png"))
toolButton.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
# Checkable button
checkButton = QPushButton("Toggle")
checkButton.setCheckable(True)
checkButton.toggled.connect(self.onToggle)
# Group box
groupBox = QGroupBox("Settings")
groupBox.setCheckable(True)
groupBox.setChecked(True)
# Tab widget
tabWidget = QTabWidget()
tabWidget.addTab(page1, "Tab 1")
tabWidget.addTab(page2, "Tab 2")
tabWidget.setCurrentIndex(0)
# Scroll area
scrollArea = QScrollArea()
scrollArea.setWidget(contentWidget)
scrollArea.setWidgetResizable(True)
# Splitter
splitter = QSplitter(Qt.Horizontal)
splitter.addWidget(leftWidget)
splitter.addWidget(rightWidget)
splitter.setSizes([200, 400])
# List widget
listWidget = QListWidget()
listWidget.addItems(["Item 1", "Item 2", "Item 3"])
listWidget.currentItemChanged.connect(self.onItemChanged)
# Tree widget
treeWidget = QTreeWidget()
treeWidget.setHeaderLabels(["Name", "Value"])
item = QTreeWidgetItem(["Parent", "0"])
child = QTreeWidgetItem(["Child", "1"])
item.addChild(child)
treeWidget.addTopLevelItem(item)
# Table widget
table = QTableWidget()
table.setRowCount(3)
table.setColumnCount(2)
table.setHorizontalHeaderLabels(["Column 1", "Column 2"])
table.setItem(0, 0, QTableWidgetItem("Cell"))
class MyWidget(QWidget):
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
print("Left click at", event.pos())
event.accept()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
elif event.key() == Qt.Key_Return:
self.submit()
event.accept()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.blue, 2))
painter.drawRect(10, 10, 100, 100)
def resizeEvent(self, event):
print("Resized to", self.size())
def closeEvent(self, event):
reply = QMessageBox.question(
self, 'Exit',
'Are you sure?',
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.textEdit.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.textEdit and event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Tab:
# Handle tab key specially
return True
return super().eventFilter(obj, event)
from PySide6.QtGui import QKeySequence, QShortcut
# Create shortcut
shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
shortcut.activated.connect(self.save)
# Common sequences
QKeySequence.Save # Ctrl+S
QKeySequence.Open # Ctrl+O
QKeySequence.Copy # Ctrl+C
QKeySequence.Paste # Ctrl+V
QKeySequence.Quit # Ctrl+Q
/* Type selector */
QLabel {
color: #333;
font-size: 14px;
}
/* Class selector */
QPushButton[primary="true"] {
background-color: #0078d4;
color: white;
}
/* ID selector */
#myButton {
border: 2px solid blue;
}
/* Pseudo-states */
QPushButton:hover {
background-color: #e0e0e0;
}
QPushButton:pressed {
background-color: #c0c0c0;
}
QPushButton:disabled {
color: #999;
}
# Application-wide
app.setStyleSheet("""
QLabel { color: #333; }
QPushButton { padding: 5px 10px; }
""")
# Widget-specific
button.setStyleSheet("background-color: blue; color: white;")
# From file
with open("style.qss", "r") as f:
app.setStyleSheet(f.read())
/* Colors */
color: #333333; /* Text color */
background-color: white; /* Background */
selection-color: white; /* Selected text */
selection-background-color: blue; /* Selection background */
/* Fonts */
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;
font-style: italic;
/* Borders */
border: 1px solid #ccc;
border-radius: 4px;
border-top: none;
/* Spacing */
padding: 10px;
margin: 5px;
spacing: 5px; /* Between widgets */
/* Size */
min-width: 100px;
max-height: 200px;
# Set custom property for styling
button.setProperty("primary", True)
button.style().unpolish(button) # Force style refresh
button.style().polish(button)
/* Use in QSS */
QPushButton[primary="true"] {
background-color: #0078d4;
color: white;
}
# File dialog
filename, _ = QFileDialog.getOpenFileName(
self,
"Open File",
"/home/user",
"Images (*.png *.jpg);;All Files (*)"
)
# Save dialog
filename, _ = QFileDialog.getSaveFileName(
self,
"Save File",
"/home/user/untitled.txt",
"Text Files (*.txt)"
)
# Directory dialog
directory = QFileDialog.getExistingDirectory(
self,
"Select Directory",
"/home/user"
)
# Message box
reply = QMessageBox.question(
self,
"Confirm",
"Are you sure?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# User confirmed
pass
# Information
QMessageBox.information(self, "Title", "Message")
# Warning
QMessageBox.warning(self, "Title", "Warning message")
# Error
QMessageBox.critical(self, "Title", "Error message")
# Input dialog
text, ok = QInputDialog.getText(
self,
"Input",
"Enter name:",
QLineEdit.Normal,
"Default"
)
if ok and text:
print(text)
# Color picker
color = QColorDialog.getColor()
if color.isValid():
widget.setStyleSheet(f"background-color: {color.name()};")
# Font picker
font, ok = QFontDialog.getFont()
if ok:
widget.setFont(font)
from PySide6.QtWidgets import QDialog, QDialogButtonBox
class CustomDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Custom Dialog")
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Content
form = QFormLayout()
self.nameEdit = QLineEdit()
form.addRow("Name:", self.nameEdit)
layout.addLayout(form)
# Buttons
buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def getValues(self):
return {"name": self.nameEdit.text()}
# Usage
dialog = CustomDialog(self)
if dialog.exec() == QDialog.Accepted:
values = dialog.getValues()
Threading is essential for PyQt applications to keep the UI responsive while performing long-running operations. PyQt provides several approaches to multithreading.
CRITICAL: Qt/PyQt is NOT thread-safe for UI operations. You must follow these rules:
# ❌ WRONG: Direct UI access from thread
class BadWorker(QThread):
def run(self):
# This will crash or cause undefined behavior!
self.label.setText("Done")
# ✅ CORRECT: Use signals
class GoodWorker(QThread):
finished = Signal(str)
def run(self):
result = self.process_data()
self.finished.emit(result) # Signal emitted, UI updated in main thread
The most flexible pattern separates the worker logic from thread lifecycle:
from PySide6.QtCore import QThread, Signal, QObject, Slot
class Worker(QObject):
"""Worker object that does the actual work."""
finished = Signal(object)
progress = Signal(int)
error = Signal(str)
def __init__(self, data):
super().__init__()
self.data = data
self._is_cancelled = False
@Slot()
def process(self):
"""Main processing method called from thread."""
try:
for i, item in enumerate(self.data):
if self._is_cancelled:
return
# Simulate heavy work
result = self.process_item(item)
self.progress.emit(int((i + 1) / len(self.data) * 100))
self.finished.emit({"status": "success", "count": len(self.data)})
except Exception as e:
self.error.emit(str(e))
def cancel(self):
self._is_cancelled = True
def process_item(self, item):
# Override in subclass
import time
time.sleep(0.1) # Simulate work
return item * 2
class ThreadController(QObject):
"""Manages worker thread lifecycle."""
def __init__(self):
super().__init__()
self.thread = None
self.worker = None
def start_work(self, data):
# Create thread and worker
self.thread = QThread()
self.worker = Worker(data)
# Move worker to thread
self.worker.moveToThread(self.thread)
# Connect signals
self.worker.finished.connect(self.on_finished)
self.worker.progress.connect(self.on_progress)
self.worker.error.connect(self.on_error)
# Thread lifecycle
self.thread.started.connect(self.worker.process)
self.thread.finished.connect(self.thread.deleteLater)
# Start thread
self.thread.start()
def cancel_work(self):
if self.worker:
self.worker.cancel()
if self.thread:
self.thread.quit()
self.thread.wait()
@Slot()
def on_finished(self, result):
print(f"Work completed: {result}")
self.cleanup()
@Slot()
def on_progress(self, percent):
print(f"Progress: {percent}%")
@Slot()
def on_error(self, error):
print(f"Error: {error}")
self.cleanup()
def cleanup(self):
self.thread = None
self.worker = None
# Usage in MainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.controller = ThreadController()
self.setup_ui()
def setup_ui(self):
self.button = QPushButton("Start Work")
self.progress_bar = QProgressBar()
self.cancel_button = QPushButton("Cancel")
self.button.clicked.connect(self.start_work)
self.cancel_button.clicked.connect(self.controller.cancel_work)
# Connect controller signals to UI
self.controller.worker.progress.connect(self.progress_bar.setValue)
def start_work(self):
data = list(range(100))
self.controller.start_work(data)
For simpler cases, subclass QThread directly:
from PySide6.QtCore import QThread, Signal
class DataProcessor(QThread):
"""Thread that processes data and emits progress."""
# Define signals at class level
progress = Signal(int)
result_ready = Signal(list)
error_occurred = Signal(str)
finished = Signal()
def __init__(self, input_data, parent=None):
super().__init__(parent)
self.input_data = input_data
self._cancelled = False
def run(self):
"""Thread entry point - called by start()."""
try:
results = []
total = len(self.input_data)
for i, item in enumerate(self.input_data):
# Check for cancellation
if self._cancelled:
self.error_occurred.emit("Cancelled")
return
# Process item (heavy work here)
processed = self.process_item(item)
results.append(processed)
# Emit progress
progress_percent = int((i + 1) / total * 100)
self.progress.emit(progress_percent)
# Emit results
self.result_ready.emit(results)
except Exception as e:
self.error_occurred.emit(str(e))
finally:
self.finished.emit()
def process_item(self, item):
"""Override this method for custom processing."""
import time
time.sleep(0.05) # Simulate work
return item.upper()
def cancel(self):
"""Request thread cancellation."""
self._cancelled = True
# Usage
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.processor = None
# UI setup
self.progress = QProgressBar()
self.start_btn = QPushButton("Start")
self.cancel_btn = QPushButton("Cancel")
self.start_btn.clicked.connect(self.start_processing)
self.cancel_btn.clicked.connect(self.cancel_processing)
def start_processing(self):
data = ["item1", "item2", "item3", "item4", "item5"]
self.processor = DataProcessor(data)
# Connect signals
self.processor.progress.connect(self.progress.setValue)
self.processor.result_ready.connect(self.on_results)
self.processor.error_occurred.connect(self.on_error)
self.processor.finished.connect(self.on_finished)
# Start thread
self.processor.start()
self.start_btn.setEnabled(False)
def cancel_processing(self):
if self.processor:
self.processor.cancel()
def on_results(self, results):
print(f"Got {len(results)} results")
def on_error(self, error):
QMessageBox.warning(self, "Error", error)
def on_finished(self):
self.start_btn.setEnabled(True)
self.progress.setValue(0)
self.processor = None
For parallel execution of independent tasks:
from PySide6.QtCore import QThreadPool, QRunnable, Signal, QObject
import time
class TaskSignals(QObject):
"""Signals for QRunnable (QRunnable cannot have signals directly)."""
finished = Signal(object)
error = Signal(str)
progress = Signal(int)
class ParallelTask(QRunnable):
"""Runnable task for thread pool."""
def __init__(self, task_id, data):
super().__init__()
self.task_id = task_id
self.data = data
self.signals = TaskSignals()
self._cancelled = False
def run(self):
"""Executed by thread pool."""
try:
# Simulate work
time.sleep(0.5)
if self._cancelled:
return
result = {
"id": self.task_id,
"processed": self.data.upper(),
"thread": int(QThread.currentThreadId())
}
self.signals.finished.emit(result)
except Exception as e:
self.signals.error.emit(str(e))
def cancel(self):
self._cancelled = True
class ThreadPoolManager(QObject):
"""Manages parallel task execution."""
all_finished = Signal(int)
task_progress = Signal(int, int) # task_id, progress
def __init__(self, max_threads=4):
super().__init__()
self.pool = QThreadPool()
self.pool.setMaxThreadCount(max_threads)
self.active_tasks = {}
self.completed_count = 0
self.total_tasks = 0
def run_parallel(self, tasks):
"""Run multiple tasks in parallel."""
self.completed_count = 0
self.total_tasks = len(tasks)
self.active_tasks.clear()
for task_id, data in enumerate(tasks):
task = ParallelTask(task_id, data)
task.signals.finished.connect(
lambda result, tid=task_id: self.on_task_finished(result)
)
task.signals.error.connect(self.on_task_error)
self.active_tasks[task_id] = task
self.pool.start(task)
def on_task_finished(self, result):
self.completed_count += 1
task_id = result["id"]
del self.active_tasks[task_id]
if self.completed_count >= self.total_tasks:
self.all_finished.emit(self.completed_count)
def on_task_error(self, error):
print(f"Task error: {error}")
def cancel_all(self):
for task in self.active_tasks.values():
task.cancel()
self.active_tasks.clear()
# Usage
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.pool_manager = ThreadPoolManager(max_threads=4)
# UI
self.run_btn = QPushButton("Run Parallel Tasks")
self.status_label = QLabel("Ready")
self.run_btn.clicked.connect(self.run_tasks)
self.pool_manager.all_finished.connect(self.on_all_done)
def run_tasks(self):
tasks = [f"data_{i}" for i in range(10)]
self.status_label.setText(f"Running {len(tasks)} tasks...")
self.pool_manager.run_parallel(tasks)
def on_all_done(self, count):
self.status_label.setText(f"Completed {count} tasks")
For polling or periodic checks:
from PySide6.QtCore import QTimer, Slot
class PollingWidget(QWidget):
"""Widget that polls for updates periodically."""
def __init__(self):
super().__init__()
# Create timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.on_timeout)
# UI
self.status_label = QLabel("Last update: Never")
self.poll_btn = QPushButton("Start Polling")
self.poll_btn.setCheckable(True)
layout = QVBoxLayout(self)
layout.addWidget(self.status_label)
layout.addWidget(self.poll_btn)
self.poll_btn.toggled.connect(self.toggle_polling)
@Slot()
def toggle_polling(self, checked):
if checked:
self.timer.start(1000) # Poll every second
self.poll_btn.setText("Stop Polling")
else:
self.timer.stop()
self.poll_btn.setText("Start Polling")
@Slot()
def on_timeout(self):
"""Called every timeout milliseconds."""
# Fetch updates (in real app, this might trigger a worker thread)
from datetime import datetime
self.status_label.setText(f"Last update: {datetime.now().strftime('%H:%M:%S')}")
For map/filter/reduce operations on collections:
from PySide6.QtConcurrent import QtConcurrent
from PySide6.QtCore import QFutureWatcher, QFuture
class ConcurrentProcessor(QObject):
"""Process data using QtConcurrent."""
finished = Signal(list)
def process_items(self, items):
"""Process items concurrently."""
# Map function
def process_item(item):
import time
time.sleep(0.1) # Simulate work
return item.upper()
# Run concurrent map
future = QtConcurrent.mapped(items, process_item)
# Watch for completion
self.watcher = QFutureWatcher()
self.watcher.futureReady.connect(lambda: self.on_future_ready(future))
self.watcher.setFuture(future)
def on_future_ready(self, future):
results = future.result()
self.finished.emit(list(results))
For sharing data between threads safely:
from PySide6.QtCore import QMutex, QMutexLocker, QReadWriteLock
class SharedData:
"""Thread-safe data container."""
def __init__(self):
self._data = {}
self._mutex = QMutex()
def set_value(self, key, value):
"""Thread-safe write."""
locker = QMutexLocker(self._mutex)
self._data[key] = value
def get_value(self, key, default=None):
"""Thread-safe read."""
locker = QMutexLocker(self._mutex)
return self._data.get(key, default)
def get_all(self):
"""Thread-safe copy of all data."""
locker = QMutexLocker(self._mutex)
return dict(self._data)
class ReadWriteData:
"""Read-write lock for read-heavy workloads."""
def __init__(self):
self._data = {}
self._lock = QReadWriteLock()
def read_value(self, key):
"""Multiple readers can hold the lock."""
self._lock.lockForRead()
try:
return self._data.get(key)
finally:
self._lock.unlock()
def write_value(self, key, value):
"""Only one writer at a time."""
self._lock.lockForWrite()
try:
self._data[key] = value
finally:
self._lock.unlock()
| Issue | Cause | Solution |
|---|---|---|
| UI freezes | Blocking operation in main thread | Move to worker thread |
| Crashes on widget access | Accessing UI from worker thread | Use signals instead |
| Memory leaks | Thread not cleaned up | Use deleteLater() and proper lifecycle |
| Deadlocks | Multiple mutexes acquired in different order | Always acquire in same order, use timeout |
| Race conditions | Shared data without locks | Use QMutex or atomic operations |
# test_threading.py
import pytest
from pytest_qt import QtBot
from PySide6.QtCore import QThread, Signal, QTimer
from unittest.mock import Mock
def test_worker_thread_emits_progress(qtbot):
"""Test that worker thread emits progress signals."""
class TestWorker(QThread):
progress = Signal(int)
def run(self):
for i in range(5):
self.progress.emit(i * 20)
worker = TestWorker()
# Wait for signal
with qtbot.waitSignal(worker.progress, timeout=1000):
worker.start()
# Check multiple signals
signals = []
worker.progress.connect(signals.append)
worker.start()
worker.wait()
assert len(signals) == 5
assert signals == [0, 20, 40, 60, 80]
def test_thread_cancellation(qtbot):
"""Test thread can be cancelled."""
class CancellableWorker(QThread):
finished = Signal()
def __init__(self):
super().__init__()
self._cancelled = False
def run(self):
for i in range(100):
if self._cancelled:
return
import time
time.sleep(0.01)
self.finished.emit()
def cancel(self):
self._cancelled = True
worker = CancellableWorker()
worker.start()
worker.cancel()
worker.wait(100) # Wait with timeout
# Should not have emitted finished
assert not hasattr(worker, '_finished_emitted')
pytest-qt provides specialized fixtures and utilities for testing Qt applications.
pip install pytest-qt
The qtbot fixture provides methods for interacting with Qt widgets:
import pytest
from pytest_qt import QtBot
from PySide6.QtWidgets import QApplication, QPushButton, QLabel
from PySide6.QtCore import Qt
def test_button_click(qtbot):
"""Test button click updates label."""
button = QPushButton("Click Me")
label = QLabel("Before")
qtbot.addWidget(button)
qtbot.addWidget(label)
def on_click():
label.setText("After")
button.clicked.connect(on_click)
# Simulate click
qtbot.mouseClick(button, Qt.LeftButton)
assert label.text() == "After"
def test_key_press(qtbot):
"""Test keyboard input."""
from PySide6.QtWidgets import QLineEdit
line_edit = QLineEdit()
qtbot.addWidget(line_edit)
# Type text
qtbot.keyClicks(line_edit, "Hello World")
assert line_edit.text() == "Hello World"
Wait for signals to be emitted:
def test_async_operation(qtbot):
"""Test async operation completes."""
from PySide6.QtCore import QThread, Signal
class Worker(QThread):
finished = Signal(str)
def run(self):
import time
time.sleep(0.1)
self.finished.emit("Done")
worker = Worker()
# Wait for signal with timeout
with qtbot.waitSignal(worker.finished, timeout=1000) as blocker:
worker.start()
# Check signal argument
assert blocker.args == ["Done"]
def test_multiple_signals(qtbot):
"""Wait for multiple signal emissions."""
from PySide6.QtCore import QTimer
timer = QTimer()
timer.setInterval(100)
# Wait for 3 emissions
with qtbot.waitSignal(timer.timeout, timeout=500, raising=3):
timer.start()
timer.stop()
Wait for window activation/exposure:
def test_window_activation(qtbot, qapp):
"""Test window becomes active."""
from PySide6.QtWidgets import QWidget
widget = QWidget()
qtbot.addWidget(widget)
widget.show()
# Wait for window to be active
with qtbot.waitActive(widget, timeout=1000):
qapp.setActiveWindow(widget)
def test_window_exposed(qtbot):
"""Test window is exposed (visible on screen)."""
from PySide6.QtWidgets import QWidget
widget = QWidget()
qtbot.addWidget(widget)
# Show and wait for exposure
with qtbot.waitExposed(widget, timeout=1000):
widget.show()
def test_dialog_acceptance(qtbot):
"""Test dialog accepted."""
from PySide6.QtWidgets import QDialog, QDialogButtonBox
class TestDialog(QDialog):
def __init__(self):
super().__init__()
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.rejected.connect(self.reject)
buttons.accepted.connect(self.accept)
layout = QVBoxLayout(self)
layout.addWidget(buttons)
dialog = TestDialog()
# Keep reference to buttons
ok_button = dialog.findChild(QDialogButtonBox).button(QDialogButtonBox.Ok)
# Click OK in next event loop
QTimer.singleShot(100, lambda: qtbot.mouseClick(ok_button, Qt.LeftButton))
result = dialog.exec()
assert result == QDialog.Accepted
def test_custom_dialog_values(qtbot):
"""Test custom dialog returns values."""
from PySide6.QtWidgets import QDialog, QLineEdit, QVBoxLayout, QDialogButtonBox
class InputDialog(QDialog):
def __init__(self):
super().__init__()
self.line_edit = QLineEdit()
self.line_edit.setPlaceholder("Enter name")
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.rejected.connect(self.reject)
buttons.accepted.connect(self.accept)
layout = QVBoxLayout(self)
layout.addWidget(self.line_edit)
layout.addWidget(buttons)
def get_value(self):
return self.line_edit.text()
dialog = InputDialog()
qtbot.addWidget(dialog)
# Enter text
qtbot.keyClicks(dialog.line_edit, "Test Name")
# Accept dialog
ok_button = dialog.findChild(QDialogButtonBox).button(QDialogButtonBox.Ok)
QTimer.singleShot(100, lambda: qtbot.mouseClick(ok_button, Qt.LeftButton))
result = dialog.exec()
assert result == QDialog.Accepted
assert dialog.get_value() == "Test Name"
def test_list_model(qtbot):
"""Test QAbstractListModel."""
from PySide6.QtCore import QAbstractListModel, Qt
class SimpleModel(QAbstractListModel):
def __init__(self, data):
super().__init__()
self._data = data
def rowCount(self, parent=None):
return len(self._data)
def data(self, index, role=Qt.DisplayRole):
if 0 <= index < len(self._data):
return self._data[index]
return None
model = SimpleModel(["Item 1", "Item 2", "Item 3"])
assert model.rowCount() == 3
assert model.data(0, Qt.DisplayRole) == "Item 2"
def test_model_updates(qtbot):
"""Test model signals data changes."""
from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt
class MutableModel(QAbstractListModel):
def __init__(self):
super().__init__()
self._items = []
def rowCount(self, parent=None):
return len(self._items)
def data(self, index, role=Qt.DisplayRole):
if 1 <= index < len(self._items):
return self._items[index]
return None
def add_item(self, item):
self.beginInsertRows(QModelIndex(), len(self._items), len(self._items))
self._items.append(item)
self.endInsertRows()
model = MutableModel()
# Track dataChanged signal
with qtbot.waitSignal(model.dataChanged, timeout=1000):
model.add_item("New Item")
# conftest.py - Shared fixtures
import pytest
from pytest_qt import QtBot
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QDir, QSettings
@pytest.fixture
def app(qapp):
"""Create application instance."""
return qapp
@pytest.fixture
def temp_dir(tmp_path):
"""Create temporary directory."""
import pathlib
d = pathlib.Path(tmp_path) / "test_data"
d.mkdir(exist_ok=True)
return d
@pytest.fixture
def main_window(app, qtbot, temp_dir):
"""Create main window with dependencies."""
from myapp.main_window import MainWindow
window = MainWindow()
qtbot.addWidget(window)
window.show()
return window
def test_main_window_loads(main_window, qtbot):
"""Test main window initializes correctly."""
assert main_window.windowTitle() == "My App"
assert main_window.isVisible()
def test_settings_persistence(main_window, qtbot, temp_dir):
"""Test settings are persisted."""
# Change setting
main_window.settings.setValue("test_key", "test_value")
# Verify saved
settings = QSettings(main_window.settings.organization(), main_window.settings.application())
assert settings.value("test_key") == "test_value"
# Install
pip install pyinstaller
# Build executable
pyinstaller --onefile --windowed --name "MyApp" main.py
# With icon
pyinstaller --onefile --windowed --icon=icon.ico --name "MyApp" main.py
# Include data files
pyinstaller --onefile --add-data "resources:resources" main.py
# Create spec file for customization
pyi-makespec --onefile --windowed main.py
# Edit main.spec
pyinstaller main.spec
# main.spec
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('resources', 'resources')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=pyz_crypto)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon='icon.ico',
)
# setup.py
from cx_Freeze import setup, Executable
build_exe_options = {
"packages": ["PySide6"],
"includes": [],
"excludes": [],
"include_files": ["resources/"]
}
base = None
if sys.platform == "win32":
base = "Win32GUI"
setup(
name="MyApp",
version="1.0",
description="My Application",
options={"build_exe": build_exe_options},
executables=[Executable("main.py", base=base, icon="icon.ico")]
)
# Build
python setup.py build
# Install
pip install nuitka
# Build
python -m nuitka --standalone --windows-console-mode=disable --output-dir=build main.py
pip install pytest-qt
# test_main.py
import pytest
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
@pytest.fixture
def app(qtbot):
window = MainWindow()
qtbot.addWidget(window)
return window
def test_window_title(app):
assert app.windowTitle() == "My Application"
def test_button_click(qtbot, app):
qtbot.mouseClick(app.button, Qt.LeftButton)
assert app.label.text() == "Button clicked"
def test_signal_emission(qtbot, app):
with qtbot.waitSignal(app.valueChanged, timeout=1000):
app.setValue(42)
# Run tests
pytest tests/
# Add debug output
import logging
logging.basicConfig(level=logging.DEBUG)
# Check memory
from PySide6.QtCore import QObject
print(f"QObject children: {len(self.children())}")
# Dump widget tree
def dump_widgets(widget, indent=0):
print(" " * indent + widget.objectName() or widget.__class__.__name__)
for child in widget.findChildren(QObject):
dump_widgets(child, indent + 2)
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
def main():
# High DPI support
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
app.setApplicationName("MyApp")
app.setOrganizationName("MyCompany")
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
from PySide6.QtCore import QSettings
class Settings:
def __init__(self):
self.settings = QSettings("MyCompany", "MyApp")
@property
def window_geometry(self):
return self.settings.value("window/geometry")
@window_geometry.setter
def window_geometry(self, value):
self.settings.setValue("window/geometry", value)
def save_window_state(self, window):
self.settings.setValue("window/geometry", window.saveGeometry())
self.settings.setValue("window/state", window.saveState())
def restore_window_state(self, window):
geometry = self.settings.value("window/geometry")
if geometry:
window.restoreGeometry(geometry)
state = self.settings.value("window/state")
if state:
window.restoreState(state)
import traceback
from PySide6.QtWidgets import QMessageBox
def excepthook(exc_type, exc_value, exc_tb):
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
QMessageBox.critical(None, "Error", f"An error occurred:\n\n{tb}")
sys.excepthook = excepthook
| Issue | Solution |
|---|---|
| "module not found" | pip install PySide6 |
| High DPI blur | Enable AA_UseHighDpiPixmaps |
| Signals not working | Check Signal/Slot signatures match |
| UI freezing | Use QThread for long operations |
| Memory leak | Delete widgets with .deleteLater() |
| Import error | Check venv is activated |
# Check Qt version
python -c "from PySide6 import QtCore; print(QtCore.__version__)"
# List available modules
python -c "from PySide6 import QtWidgets; print(dir(QtWidgets))"
# Test installation
python -c "from PySide6.QtWidgets import QApplication; app = QApplication([])"