Key Highlights:
Here’s a concise HTML-formatted summary of the article in 3-5 bullet points:
- Problem: Secondary windows in PyQt6 cannot directly modify background threads started from the main window, while main window buttons can.
- Solution: Use Qt’s signal-slot system for cross-thread communication instead of direct method calls, ensuring thread safety.
- Implementation:
- Worker threads expose slots (e.g.,
set_value) - Secondary windows emit signals (e.g.,
value_changed) - Main window connects secondary window signals to worker slots
- Worker threads expose slots (e.g.,
- Key Benefit: Decouples components – secondary windows need no knowledge of worker threads, only signal interfaces.
- Best Practice:
QThreadwithmoveToThread()is preferred overQThreadPoolfor long-running tasks needing two-way GUI communication.
Here’s a rewritten version of your article with improved clarity, structure, and readability while maintaining the original meaning:
I’m working on a PyQt6 application with a main window that launches background threads (for tasks like handling GPIO data). The main window can open secondary windows through buttons. However, I’ve noticed that while buttons in the main window can control the background threads, buttons in secondary windows can’t. How can I enable communication between secondary windows and threads started from the main window?
This is a common challenge in multi-window PyQt6 applications. Fortunately, Qt’s signal and slot system provides a thread-safe solution. The key is that your secondary window doesn’t need direct access to the thread or worker object—both just need to use the same signals for communication.
Why Direct Access Doesn’t Work
When you create a background thread from the main window, the main window typically stores a reference to that thread. Secondary windows created by the main window don’t automatically have access to these references. Even if you could access them (through methods like .parent()), directly calling thread methods across windows is problematic because:
- It creates tight coupling between components
- Changes to the parent window structure require updates to child windows
- Direct cross-thread method calls aren’t thread-safe
The Solution: Signals and Slots
Qt’s signal-slot system automatically handles cross-thread communication safely. When a signal in one thread connects to a slot in another, Qt queues the call and delivers it properly. Here’s how to implement this:
1. Creating a Background Worker
First, we’ll create a worker class that runs in a background thread. This example simulates data handling while accepting GUI commands:
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
import time
class Worker(QObject):
"""Background worker handling data operations"""
data_updated = pyqtSignal(str)
def __init__(self):
super().__init__()
self.running = True
self.current_value = 0
@pyqtSlot()
def run(self):
"""Continuous data processing"""
while self.running:
self.current_value += 1
self.data_updated.emit(f"Data: {self.current_value}")
time.sleep(1)
@pyqtSlot(int)
def set_value(self, value):
"""Receive GUI updates"""
self.current_value = value
self.data_updated.emit(f"Value set to: {self.current_value}")
2. Building the Secondary Window
The secondary window emits signals without knowing about the worker:
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QSpinBox, QLabel
from PyQt6.QtCore import pyqtSignal
class SecondaryWindow(QWidget):
"""Window for sending values to worker"""
value_changed = pyqtSignal(int)
def __init__(self):
super().__init__()
self.setWindowTitle("Secondary Window")
layout = QVBoxLayout()
self.label = QLabel("Set a new value for the worker:")
layout.addWidget(self.label)
self.spinbox = QSpinBox()
self.spinbox.setRange(0, 1000)
layout.addWidget(self.spinbox)
self.button = QPushButton("Send to Worker")
self.button.clicked.connect(self.send_value)
layout.addWidget(self.button)
self.setLayout(layout)
def send_value(self):
self.value_changed.emit(self.spinbox.value())
3. Connecting Everything in the Main Window
The main window orchestrates all connections:
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, QLabel, QWidget
from PyQt6.QtCore import QThread
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Main Window")
# UI Setup
layout = QVBoxLayout()
self.status_label = QLabel("Waiting for data...")
layout.addWidget(self.status_label)
self.open_button = QPushButton("Open Secondary Window")
self.open_button.clicked.connect(self.open_secondary)
layout.addWidget(self.open_button)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
# Window and thread management
self.secondary_window = None
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
# Signal connections
self.thread.started.connect(self.worker.run)
self.worker.data_updated.connect(self.update_status)
self.thread.start()
def update_status(self, text):
self.status_label.setText(text)
def open_secondary(self):
if not self.secondary_window:
self.secondary_window = SecondaryWindow()
# Critical connection between windows and threads
self.secondary_window.value_changed.connect(self.worker.set_value)
self.secondary_window.show()
def closeEvent(self, event):
self.worker.running = False
self.thread.quit()
self.thread.wait()
super().closeEvent(event)
Key Connection Explained
The magic happens in this line:
self.secondary_window.value_changed.connect(self.worker.set_value)
This connects the secondary window’s signal (running in the GUI thread) to the worker’s slot (in the background thread). Qt automatically uses a queued connection when it detects different threads.
Why the Original Approach Failed
The main window worked because it established direct signal-slot connections during initialization. Secondary windows failed because:
- They lacked connections to the worker
- They attempted direct method calls across threads
Implementation Notes
For long-running tasks needing two-way GUI communication (like GPIO handling), QThread with moveToThread() is generally better than QThreadPool because:
- It provides a proper event loop in the background thread
- Signals and slots work naturally in both directions
When you run this complete example, the main window displays incrementing values. The secondary window can safely modify these values through signals, demonstrating proper cross-thread communication without direct method calls.

