如何让我的PyQt应用在控制台被终止时正常退出(Ctrl-C)?

85 投票
9 回答
52225 浏览
提问于 2025-04-16 11:28

我想知道,怎样才能让我的PyQt应用在控制台被强制结束时(比如按Ctrl+C)正常退出?

现在,我的PyQt应用对SIGINT(也就是Ctrl+C)没有任何反应,完全不理会。我希望它能好好地退出,而不是无视这个信号。我该怎么做呢?

9 个回答

15

18.8.1.1. Python信号处理器的执行

在Python中,信号处理器并不是在底层的(C语言写的)信号处理器内部执行的。相反,底层信号处理器会设置一个标志,告诉虚拟机在稍后的某个时刻执行相应的Python信号处理器(比如在下一个字节码指令时)。这有一些影响:
[...]
如果有一个纯C语言实现的长时间运行的计算(比如在大量文本上进行正则表达式匹配),那么这个计算可能会在任意时间内不被打断,无论收到什么信号。Python的信号处理器会在计算完成后被调用。

Qt的事件循环是用C(或C++)实现的。这意味着,当它运行并且没有调用任何Python代码(例如,通过连接到Python槽的Qt信号),信号会被记录,但Python的信号处理器不会被调用。

但是,从Python 2.6开始,以及在Python 3中,你可以让Qt在收到带有处理器的信号时运行一个Python函数,这可以通过signal.set_wakeup_fd()来实现。

之所以可以这样做,是因为与文档中所说的不同,底层信号处理器不仅仅是为虚拟机设置一个标志,它还可以向通过set_wakeup_fd()设置的文件描述符写入一个字节。Python 2写入一个空字节,Python 3则写入信号编号。

因此,通过子类化一个接受文件描述符并提供readReady()信号的Qt类,比如QAbstractSocket,每当收到一个信号(带有处理器)时,事件循环就会执行一个Python函数,这样信号处理器几乎可以立即执行,而不需要定时器:

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_())
47

如果你只是想让按下 ctrl-c 时直接关闭应用程序,而不需要优雅地处理这个过程,那么你可以参考这个链接: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_()

看起来这个方法在 Linux、Windows 和 OSX 上都能用,不过我目前只在 Linux 上测试过,结果是可以的。

58

17.4. signal — 设置异步事件的处理程序

虽然从Python用户的角度来看,信号处理程序是异步调用的,但它们只能在Python解释器的“原子”指令之间发生。这意味着在进行一些长时间计算(比如在大文本上进行正则表达式匹配)时,如果有信号到达,可能会被延迟一段时间。

这就意味着,当Qt的事件循环在运行时,Python是无法处理信号的。只有在Python解释器运行的时候(比如当QApplication退出,或者从Qt调用Python函数时),信号处理程序才会被调用。

一个解决办法是使用QTimer,让解释器不时地运行一下。

需要注意的是,在下面的代码中,如果没有打开的窗口,应用程序会在消息框之后退出,无论用户选择什么,因为QApplication.quitOnLastWindowClosed() == True。这种行为是可以改变的。

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_())

另一个可能的解决方案,正如LinearOrbit所指出的,是使用signal.signal(signal.SIGINT, signal.SIG_DFL),但这样就不能使用自定义的处理程序了。

撰写回答