...

PyQt6 Thread Communication Between Windows

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
  • Key Benefit: Decouples components – secondary windows need no knowledge of worker threads, only signal interfaces.
  • Best Practice: QThread with moveToThread() is preferred over QThreadPool for 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.

Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.