Changelog
Bring up the WYSIWYG designer:
user:~$ uv tool install pyqt6-tools --python 3.9 --with setuptools user:~$ pyqt6-tools designer
Remember to set the top-level layout: click on the widget background and select layout from context menu.
import signal import socket import sys import typing from PySide6.QtNetwork import QAbstractSocket from PySide6.QtCore import QObject, Signal class SignalQuitHandler(QAbstractSocket): """Allow Qt event loop to return control upon signal interrupt. In order for an emitted signal to be handled by Python, the Python main thread must be given control. The C++ Qt event loop has no Python runtime dependency, so an asynchronous method is required to inform Qt to return control to Python. This relies on the Python signal handler writing a byte (corresponding to the signal number) via 'set_wakeup_fd()' into attached file descriptor. This byte message can be passed through a socket connection into Qt, which will emit a 'readyRead' signal that runs a Python handler. References: [1]: Original implementation, <https://stackoverflow.com/a/37229299> [2]: Implementation with SCTP, <https://stackoverflow.com/a/65802260> Note: If compatibility with other signal handlers utilizing 'set_wakeup_fd()' as well is required, store the returned old file descriptor and reassign back to 'set_wakeup_fd()' upon handler deletion. UDP protocol is preferred over TCP/SCTP since dropped packets are not a priority, with faster handling. """ interrupt = Signal(int) def __init__(self, parent: QObject): """ Propagates system signals from Python to QEventLoop """ super().__init__(QAbstractSocket.SocketType.UdpSocket, parent) self.writer, self.reader = socket.socketpair(type=socket.SOCK_DGRAM) # Python signal handler hook writes to socket self.writer.setblocking(False) self.old_fd = signal.set_wakeup_fd( self.writer.fileno(), warn_on_full_buffer=False, ) # Qt hook reads from socket and executes a Python function self.setSocketDescriptor(self.reader.fileno()) self.readyRead.connect(lambda: None) # trigger and ignore exception self.readyRead.connect(self.handle) def handle(self): """Emits signalReceived with signal interrupt number.""" data = self.readData(1) # needed to unblock 'readyRead' if data == -1: # no bytes to be read return signal = int.from_bytes( typing.cast(str, data).encode(), sys.byteorder, # any byteorder is OK ) self.interrupt.emit(signal)
Usage:
app = QApplication(sys.argv) handler = SignalQuitHandler(app) signal.signal(signal.SIGINT, lambda *args: app.quit()) handler.interrupt.connect(lambda sig: app.quit()) # alternative sys.exit(app.exec())