Coder Perfect

What is the proper approach to terminate my PyQt program from the console (Ctrl-C)?

Problem

What is the proper approach to terminate my PyQt program from the console (Ctrl-C)?

Currently (I have done nothing special to handle unix signals), my PyQt application ignores SIGINT (Ctrl+C). I’d like it to be polite and leave when it’s killed. How should I do that?

Asked by static_rtti

Solution #1

As a result, Python is unable to handle signals while the Qt event loop is active. The signal handler is only called when the Python interpreter is running (when the QApplication is closed or when a Python function is called from Qt).

Using a QTimer to run the interpreter from time to time is one way.

Note that, in the code below, if there are no open windows, the application will quit after the message box regardless of the user’s choice because QApplication.quitOnLastWindowClosed() == True. This is a behavior that can be altered.

import signal
import sys

from PyQt4.QtCore import QTimer
from PyQt4.QtGui import QApplication, QMessageBox

# Your code here

def sigint_handler(*args):
    """Handler for the SIGINT signal."""
    sys.stderr.write('\r')
    if QMessageBox.question(None, '', "Are you sure you want to quit?",
                            QMessageBox.Yes | QMessageBox.No,
                            QMessageBox.No) == QMessageBox.Yes:
        QApplication.quit()

if __name__ == "__main__":
    signal.signal(signal.SIGINT, sigint_handler)
    app = QApplication(sys.argv)
    timer = QTimer()
    timer.start(500)  # You may change this if you wish.
    timer.timeout.connect(lambda: None)  # Let the interpreter run each 500 ms.
    # Your code here.
    sys.exit(app.exec_())

Signal.signal(signal.SIGINT, signal.SIG DFL) is another option, as suggested by LinearOrbit, however it does not support custom handlers.

Answered by Artur Gaspar

Solution #2

If you just want ctrl-c to end the application without being “kind” or gracious about it, you can use this: http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg13758.html

import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)

import sys
from PyQt4.QtCore import QCoreApplication
app = QCoreApplication(sys.argv)
app.exec_()

This is supposed to work on Linux, Windows, and OSX, but I’ve only tried it on Linux so far (and it works).

Answered by LinearOrbit

Solution #3

The event loop in Qt is written in C++. That is, when it is running and no Python code is being called (for example, by a Qt signal tied to a Python slot), the signals are noted but the Python signal handlers are not being executed.

But, since Python 2.6 and in Python 3 you can cause Qt to run a Python function when a signal with a handler is received using signal.set_wakeup_fd().

This is conceivable because, contrary to the documentation, the low-level signal handler can write a byte into the file descriptor provided by set wakeup fd in addition to setting a flag for the virtual machine (). Python 2 writes the signal number, while Python 3 writes the NUL byte.

So, by subclassing a Qt class that receives a file descriptor and offers a readReady() signal, such as QAbstractSocket, the event loop will run a Python function every time a signal (with a handler) is received, permitting the signal handler to execute almost instantly without the use of timers:

import sys, signal, socket
from PyQt4 import QtCore, QtNetwork

class SignalWakeupHandler(QtNetwork.QAbstractSocket):

    def __init__(self, parent=None):
        super().__init__(QtNetwork.QAbstractSocket.UdpSocket, parent)
        self.old_fd = None
        # Create a socket pair
        self.wsock, self.rsock = socket.socketpair(type=socket.SOCK_DGRAM)
        # Let Qt listen on the one end
        self.setSocketDescriptor(self.rsock.fileno())
        # And let Python write on the other end
        self.wsock.setblocking(False)
        self.old_fd = signal.set_wakeup_fd(self.wsock.fileno())
        # First Python code executed gets any exception from
        # the signal handler, so add a dummy handler first
        self.readyRead.connect(lambda : None)
        # Second handler does the real handling
        self.readyRead.connect(self._readSignal)

    def __del__(self):
        # Restore any old handler on deletion
        if self.old_fd is not None and signal and signal.set_wakeup_fd:
            signal.set_wakeup_fd(self.old_fd)

    def _readSignal(self):
        # Read the written byte.
        # Note: readyRead is blocked from occuring again until readData()
        # was called, so call it, even if you don't need the value.
        data = self.readData(1)
        # Emit a Qt signal for convenience
        self.signalReceived.emit(data[0])

    signalReceived = QtCore.pyqtSignal(int)

app = QApplication(sys.argv)
SignalWakeupHandler(app)

signal.signal(signal.SIGINT, lambda sig,_: app.quit())

sys.exit(app.exec_())

Answered by cg909

Solution #4

I figured out a way to do it. The idea is to make Qt process events often enough such that the SIGINT signal may be caught in a Python callabe.

import signal, sys
from PyQt4.QtGui import QApplication, QWidget # also works with PySide

# You HAVE TO reimplement QApplication.event, otherwise it does not work.
# I believe that you need some python callable to catch the signal
# or KeyboardInterrupt exception.
class Application(QApplication):
    def event(self, e):
        return QApplication.event(self, e)

app = Application(sys.argv)

# Connect your cleanup function to signal.SIGINT
signal.signal(signal.SIGINT, lambda *a: app.quit())
# And start a timer to call Application.event repeatedly.
# You can change the timer parameter as you like.
app.startTimer(200)

w = QWidget()
w.show()
app.exec_()

Answered by parkouss

Solution #5

When the terminal window was in focus, Artur Gaspar’s solution worked for me, but not when the GUI was in focus. I had to define the following function in the class to get my GUI (which inherits from QWidget) to close:

def keyPressEvent(self,event):
    if event.key() == 67 and (event.modifiers() & QtCore.Qt.ControlModifier):
        sigint_handler()

Checking if the event key is 67 confirms that the key ‘c’ was pushed. The event modifiers are then checked to see if ctrl was pressed when ‘c’ was released.

Answered by qwerty9967

Post is based on https://stackoverflow.com/questions/4938723/what-is-the-correct-way-to-make-my-pyqt-application-quit-when-killed-from-the-co