How to Handle Button Click Events Using Signals and Slots in PyQt6?

After spending over a decade developing desktop applications, I will say that event handling is essential for creating interactive PyQt6 applications. The signal-slot mechanism is the backbone of PyQt6’s event system. In this tutorial, I will explain how to handle button click events using signals and slots in PyQt6 with examples and screenshots.

Handle Button Click Events Using Signals and Slots in PyQt6

Before getting into implementation, let’s understand what signals and slots are in PyQt6.

Signals are emitted by widgets when something happens – like a button being clicked, a slider being moved, or text being edited. Signals are essentially notifications that something has occurred.

Slots are methods that respond to signals. When a signal is emitted, the connected slot(s) will be executed.

This signal-slot mechanism is what allows your application to respond to user interactions in a clean, organized way.

Read How to Create QLineEdit Widget in PyQt6?

Method 1: Basic Signal-Slot Connection for Button Clicks

Let’s start with the most simple approach to handle button clicks:

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Basic Button Click Example")
        self.setGeometry(100, 100, 400, 200)

        # Create central widget and layout
        central_widget = QWidget()
        layout = QVBoxLayout(central_widget)

        # Create a label to display results
        self.result_label = QLabel("Click the button below")
        layout.addWidget(self.result_label)

        # Create a button
        self.button = QPushButton("Click Me!")
        layout.addWidget(self.button)

        # Connect the button's clicked signal to our custom slot
        self.button.clicked.connect(self.button_clicked)

        self.setCentralWidget(central_widget)

    # Define the slot method
    def button_clicked(self):
        self.result_label.setText("Hello from San Francisco! Button was clicked.")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

You can see the output in the screenshot below.

Handle Button Click Events Using Signals and Slots in PyQt6

In this example, I’ve:

  1. Created a simple window with a label and a button
  2. Connected the buttons clicked signal to our custom button_clicked method
  3. Updated the label’s text when the button is clicked

The key line here is self.button.clicked.connect(self.button_clicked) which establishes the signal-slot connection.

Check out How to Create QPushButton Widget in PyQt6?

Method 2: Connect Multiple Buttons to a Single Slot

In real-world applications, you’ll often have multiple buttons that need to perform similar actions. Here’s how to connect multiple buttons to a single slot:

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, 
                           QLabel, QVBoxLayout, QWidget, QHBoxLayout)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Multiple Buttons Example")
        self.setGeometry(100, 100, 500, 200)

        # Create central widget and layouts
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)

        # Create a label to display results
        self.result_label = QLabel("Select a city by clicking a button:")
        main_layout.addWidget(self.result_label)

        # Create a horizontal layout for buttons
        button_layout = QHBoxLayout()

        # Create multiple buttons for different cities
        cities = ["New York", "Los Angeles", "Chicago", "Houston", "Miami"]
        self.buttons = []

        for city in cities:
            button = QPushButton(city)
            # Connect each button to the same slot
            button.clicked.connect(self.city_button_clicked)
            button_layout.addWidget(button)
            self.buttons.append(button)

        main_layout.addLayout(button_layout)
        self.setCentralWidget(central_widget)

    # Define the slot to handle all button clicks
    def city_button_clicked(self):
        # sender() returns the button that was clicked
        button = self.sender()
        if button:
            self.result_label.setText(f"You selected: {button.text()}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

You can see the output in the screenshot below.

How to Handle Button Click Events Using Signals and Slots in PyQt6

Here, I’ve used the sender() method to identify which button was clicked. This approach is more maintainable than creating separate slot methods for each button, especially when you have many similar buttons.

Read How to Create QLabel Widget in PyQt6?

Method 3: Pass Additional Data with Lambda Functions

Sometimes you need to pass additional data to your slot function when a button is clicked. Lambda functions are perfect for this:

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, 
                           QLabel, QVBoxLayout, QWidget, QGridLayout)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Lambda Button Example")
        self.setGeometry(100, 100, 500, 300)

        # Create central widget and layouts
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)

        # Create a label to display results
        self.result_label = QLabel("Select a state and its population:")
        self.result_label.setStyleSheet("font-size: 14px; font-weight: bold;")
        main_layout.addWidget(self.result_label)

        # Create a grid layout for buttons
        button_layout = QGridLayout()

        # State data with populations
        state_data = [
            ("California", 39.5, "Sacramento"),
            ("Texas", 29.0, "Austin"),
            ("Florida", 21.5, "Tallahassee"),
            ("New York", 19.8, "Albany"),
            ("Pennsylvania", 12.8, "Harrisburg")
        ]

        # Create buttons with state names
        for i, (state, population, capital) in enumerate(state_data):
            row, col = divmod(i, 3)  # Arrange in a grid, 3 columns
            button = QPushButton(state)

            # Connect with lambda to pass additional data
            button.clicked.connect(
                lambda checked, s=state, p=population, c=capital: 
                self.state_button_clicked(s, p, c)
            )

            button_layout.addWidget(button, row, col)

        main_layout.addLayout(button_layout)
        self.setCentralWidget(central_widget)

    # Define the slot with parameters
    def state_button_clicked(self, state, population, capital):
        self.result_label.setText(
            f"{state} has a population of {population} million people. "
            f"Its capital is {capital}."
        )

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

You can see the output in the screenshot below.

Handle Button Click Events Using Signals and Slots in PyQt6 lambda function

The lambda function approach allows you to pass specific data associated with each button to your slot function. Note the use of default parameters in the lambda function (s=state, p=population, c=capital) – this is crucial to capture the current values in the loop.

Check out Basic Widgets in PyQt6

Method 4: Create Custom Signals and Slots

For more complex applications, you might want to create custom signals that carry additional data:

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, 
                           QLabel, QVBoxLayout, QWidget, QLineEdit)
from PyQt6.QtCore import pyqtSignal, pyqtSlot

class OrderWidget(QWidget):
    # Define a custom signal with order data
    order_placed = pyqtSignal(str, str, float)

    def __init__(self):
        super().__init__()

        layout = QVBoxLayout(self)

        # Create input fields
        self.product_input = QLineEdit()
        self.product_input.setPlaceholderText("Product Name")
        layout.addWidget(self.product_input)

        self.quantity_input = QLineEdit()
        self.quantity_input.setPlaceholderText("Quantity")
        layout.addWidget(self.quantity_input)

        # Create order button
        self.order_button = QPushButton("Place Order")
        self.order_button.clicked.connect(self.place_order)
        layout.addWidget(self.order_button)

    def place_order(self):
        product = self.product_input.text()
        quantity = self.quantity_input.text()

        # Simple validation
        if not product or not quantity:
            return

        try:
            # Calculate a simple price (for demo purposes)
            price = float(quantity) * 10.99

            # Emit the custom signal with order data
            self.order_placed.emit(product, quantity, price)

            # Clear the inputs
            self.product_input.clear()
            self.quantity_input.clear()

        except ValueError:
            # Handle invalid quantity
            pass

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Signals Example - Seattle Electronics")
        self.setGeometry(100, 100, 500, 300)

        # Create central widget and layout
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)

        # Create order widget
        self.order_widget = OrderWidget()
        main_layout.addWidget(self.order_widget)

        # Connect the custom signal to our slot
        self.order_widget.order_placed.connect(self.process_order)

        # Create a label to display results
        self.result_label = QLabel("Enter product details and place an order")
        main_layout.addWidget(self.result_label)

        self.setCentralWidget(central_widget)

    @pyqtSlot(str, str, float)
    def process_order(self, product, quantity, price):
        self.result_label.setText(
            f"Order Received: {quantity} x {product}\n"
            f"Total Price: ${price:.2f}"
        )

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

In this more advanced example, I’ve:

  1. Created a custom widget (OrderWidget) with its signal (order_placed)
  2. Defined the signal to carry specific data types using pyqtSignal(str, str, float)
  3. Connected this custom signal to a slot in the main window
  4. Used the @pyqtSlot decorator to specify the exact signature of our slot function

This approach is particularly valuable for complex applications where you need to pass structured data between different components.

Read How to Create Icons for Windows in PyQt6?

Method 5: Event Handling with Signal Connection Types

In PyQt6, when you connect a signal to a slot (event handler), it uses a specific connection type under the hood to manage how the signal is delivered—especially in multithreaded applications.

from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, Qt, QThread, pyqtSlot, QTimer

class Worker(QObject):
    signal = pyqtSignal()

    def __init__(self):
        super().__init__()

    def run(self):
        print("Worker thread started.")
        self.signal.emit()

class ExampleWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Signal Connection Types")
        self.setGeometry(100, 100, 400, 200)

        layout = QVBoxLayout()

        self.label = QLabel("Click the button to emit signal.")
        layout.addWidget(self.label)

        self.button = QPushButton("Emit Signal")
        self.button.clicked.connect(self.emit_signal)
        layout.addWidget(self.button)

        self.setLayout(layout)

        # Worker setup in a separate thread
        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.worker.signal.connect(self.on_signal_received, type=Qt.ConnectionType.QueuedConnection)
        self.thread.started.connect(self.worker.run)
        self.thread.start()

    @pyqtSlot()
    def on_signal_received(self):
        print("Signal received in main thread.")
        self.label.setText("Signal received!")

    def emit_signal(self):
        print("Button clicked. Starting worker thread...")
        self.thread.start()

app = QApplication([])
window = ExampleWindow()
window.show()
app.exec()
  • We define a Worker class with a signal called signal.
  • The Worker runs in a separate thread, and when emit_signal is triggered, it starts that thread.
  • The signal is connected using QueuedConnection ensuring the slot is executed in the main GUI thread.
  • We also avoid DirectConnection for GUI updates because GUI components must only be updated in the main thread

Check out Types of Windows in PyQt6

Conclusion

In this tutorial, I explained how to handle button click events using signals and slots in PyQt6. I discussed five important methods such as basic signal-slot connection for button clicks, connecting multiple buttons to a single slot, passing additional data with lambda functions, creating custom signals and slots, and event handling with signal connection types.

You may like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.