如何让我的PyQt应用在控制台被终止时正常退出(Ctrl-C)?
我想知道,怎样才能让我的PyQt应用在控制台被强制结束时(比如按Ctrl+C)正常退出?
现在,我的PyQt应用对SIGINT(也就是Ctrl+C)没有任何反应,完全不理会。我希望它能好好地退出,而不是无视这个信号。我该怎么做呢?
9 个回答
在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_())
如果你只是想让按下 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 上测试过,结果是可以的。
虽然从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)
,但这样就不能使用自定义的处理程序了。