当在控制台中使用Ctrl-C杀死应用程序时,正确的使PyQt应用程序退出的方法是什么?
目前(我没有特殊处理unix信号),我的PyQt应用程序忽略SIGINT(Ctrl+C)。我希望它能够表现得很好并在被终止时退出。我该如何做到这一点?
尽管对于 Python 用户来说,Python 信号处理程序是异步调用的,但它们只能在 Python 解释器的“原子”指令之间发生。这意味着,在纯 C 实现的长时间计算期间(例如对大量文本进行正则表达式匹配),到达的信号可能会被延迟任意长的时间。
这意味着当 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)
,但它不允许自定义处理程序。
如果您只希望使用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信号处理程序不会在低级(C语言)信号处理程序内执行。相反,低级别信号处理程序设置一个标志,告诉虚拟机在稍后的某个时间点(例如在下一条字节码指令时)执行相应的Python信号处理程序。这会产生以下影响:
[...]
纯C实现的长时间运行的计算(例如在大量文本上进行正则表达式匹配)可能会无限期地运行,而不管收到任何信号。当计算完成时,将调用Python信号处理程序。
Qt事件循环是用C++实现的。这意味着,在它运行且没有调用任何Python代码(例如通过连接到Python槽的Qt信号),信号被记录,但Python信号处理程序不会被调用。
但是,自Python 2.6以来,在Python 3中,您可以使用signal.set_wakeup_fd()
使Qt在接收到具有处理程序的信号时运行Python函数。
这是可能的,因为与文档相反,低级别信号处理程序不仅为虚拟机设置标志,而且还可以将字节写入由set_wakeup_fd()
设置的文件描述符中。Python 2写入NUL字节,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_())
socket.socketpair
,(我尝试过backports.socketpair
,但这也不起作用)。 - coldfixself.wsock.setblocking(False)
修复了我之前提到的问题。 - Michael Herrmannimport 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_()
signal.signal(signal.SIGINT, lambda *a: app.exit(-2))
- dashesyclass SignalWatchdog(QtNetwork.QAbstractSocket):
def __init__(self):
""" Propagates system signals from Python to QEventLoop """
super().__init__(QtNetwork.QAbstractSocket.SctpSocket, None)
self.writer, self.reader = socket.socketpair()
self.writer.setblocking(False)
signal.set_wakeup_fd(self.writer.fileno()) # Python hook
self.setSocketDescriptor(self.reader.fileno()) # Qt hook
self.readyRead.connect(lambda: None) # Dummy function call
def keyPressEvent(self,event):
if event.key() == 67 and (event.modifiers() & QtCore.Qt.ControlModifier):
sigint_handler()
检查事件键是否为67可以确定是否按下了“c”键。然后检查事件修饰符确定释放“c”时是否同时按下了ctrl键。
我认为我有一个更简单的解决方案:
import signal
import PyQt4.QtGui
def handleIntSignal(signum, frame):
'''Ask app to close if Ctrl+C is pressed.'''
PyQt4.QtGui.qApp.closeAllWindows()
signal.signal(signal.SIGINT, handleIntSignal)
这个代码告诉应用程序在按下ctrl+c时尝试关闭所有窗口。如果有未保存的文档,您的应用程序应该弹出保存或取消对话框,就像退出一样。
您可能还需要将QApplication信号lastWindowClosed()连接到槽quit(),以便在关闭窗口时实际退出应用程序。
您可以使用标准的Python Unix信号处理机制:
import signal
import sys
def signal_handler(signal, frame):
print 'You pressed Ctrl+C!'
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
print 'Press Ctrl+C'
while 1:
continue
在 signal_handler
中,您可以释放所有资源(关闭所有数据库会话等),并优雅地关闭应用程序。
代码示例取自 此处
_maybe_allow_interrupt
的函数,它隐藏在 matplotlib.backends.backend_qt
中。from PyQt5 import QtWidgets
from matplotlib.backends.backend_qt import _maybe_allow_interrupt
import sys
app = QtWidgets.QApplication(sys.argv)
mw = QtWidgets.QMainWindow()
mw.show()
with _maybe_allow_interrupt(app):
app.exec()
当然,由于这不是公共函数,所以在未来的matplotlib版本中可能会发生更改或消失,因此这更多地是一种“快速而脏”的解决方案。
signal.signal(signal.SIGINT, signal.SIG_DFL)
。这将用操作系统默认的信号处理程序替换Python解释器的信号处理程序。将不再引发KeyboardInterrupt异常,因此Python代码将无法捕获它们并进行清理,但即使执行编译后的代码(如Qt库),进程也会立即退出。 - ʇsәɹoɈ