当从控制台(Ctrl-C)杀死我的PyQt应用程序时,正确的退出方式是什么?

84

当在控制台中使用Ctrl-C杀死应用程序时,正确的使PyQt应用程序退出的方法是什么?

目前(我没有特殊处理unix信号),我的PyQt应用程序忽略SIGINT(Ctrl+C)。我希望它能够表现得很好并在被终止时退出。我该如何做到这一点?


7
我从未理解为什么世界上几乎每个Python脚本在结束时都需要按Control+C键,除了PyQt应用程序。毫无疑问,这其中存在着充分的原因,但最终这非常令人烦恼。 - tokland
1
@tokland:让我们一劳永逸地解决这个问题吧 :) - static_rtti
3
看起来是一个设计问题:http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg13757.html。任何涉及异常或类似的解决方案都感觉很粗糙 :-( - tokland
4
你可以在终端中使用 Ctrl + \ 来结束应用程序。 - P.R.
1
快速而不太规范的方法:在进入Qt消息循环之前使用signal.signal(signal.SIGINT, signal.SIG_DFL)。这将用操作系统默认的信号处理程序替换Python解释器的信号处理程序。将不再引发KeyboardInterrupt异常,因此Python代码将无法捕获它们并进行清理,但即使执行编译后的代码(如Qt库),进程也会立即退出。 - ʇsәɹoɈ
9个回答

57

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

尽管对于 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),但它不允许自定义处理程序。


2
似乎不起作用...Qt 似乎在我之前捕获了异常。 - static_rtti
4
你的第二个解决方案有点可行。当我按Ctrl-C时,应用程序不会立即终止,而是等待焦点返回到应用程序后才会终止,这并非预期。 - static_rtti
无论如何,感谢你的回答,如果没有更好的答案,我会尝试提出一个更具体的问题。 - static_rtti
1
谢谢!虽然看起来有点过度,但我想我会直接问PyQt的人,看看你的解决方案是否真的是唯一的 :) - static_rtti
PyQt的开发者有回应吗? - Michael Clerx
这是个技巧!但我正在使用它 :) - Michael Clerx

46

如果您只希望使用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上测试过(它是有效的)。


4
这将起作用,但请注意它会绕过您希望执行的任何清理工作,比如在finally块中调用的内容。 - Bite code

15

18.8.1.1. Python信号处理程序的执行

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

不幸的是,在Windows上无法使用socket.socketpair,(我尝试过backports.socketpair,但这也不起作用)。 - coldfix
在Windows上,套接字和其他类似文件的句柄似乎被单独处理,因此您可能需要另一种不使用套接字的构造。我不使用Windows,所以无法测试哪种方法可行。 从Python 3.5开始,似乎支持套接字(请参见https://docs.python.org/3/library/signal.html#signal.set_wakeup_fd)。 - cg909
我在尝试使用Python 3.5.3在macOS上运行时,遇到了“ValueError:fd 10必须处于非阻塞模式”的错误。 - Michael Herrmann
1
self.wsock.setblocking(False) 修复了我之前提到的问题。 - Michael Herrmann
真的很丑,但它运行得还不错(Linux,Python 3.6.8,PyQt5)。我只是把类放在一个单独的文件中。否则,我的应用程序在GUI再次获得焦点之前无法使用CTRL-C关闭。希望Qt/PyQt将来能够纳入这一点。 - brookbot

7
我发现一种方法来解决这个问题。其思路是通过强制QT处理事件并在Python中调用可捕捉SIGINT信号的函数来实现。
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_()

我是这样连接的,以便获取返回码 signal.signal(signal.SIGINT, lambda *a: app.exit(-2)) - dashesy

6
cg909 / Michael Herrmann提出的异步方法非常有趣,可以替代定时器。因此,这里提供了一个简化版本,也使用了socket.socketpair(SOCK_STREAM)的默认类型。
class 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

1
这个,连同cg909的帖子,是正确的答案。 - abey

2
当终端窗口处于焦点状态时,Artur Gaspar的答案对我有用,但在图形用户界面(GUI)处于焦点状态时无法工作。为了使我的GUI关闭(它是从QWidget继承而来的),我必须在类中定义以下函数:
def keyPressEvent(self,event):
    if event.key() == 67 and (event.modifiers() & QtCore.Qt.ControlModifier):
        sigint_handler()

检查事件键是否为67可以确定是否按下了“c”键。然后检查事件修饰符确定释放“c”时是否同时按下了ctrl键。


1

我认为我有一个更简单的解决方案:

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(),以便在关闭窗口时实际退出应用程序。


1
我没有测试过你的解决方案,但我非常确定它遇到了与上面某个解决方案相同的问题。 - static_rtti
信号在控制权返回Python解释器之前不会被捕获,即在应用程序从睡眠中返回之前不会被捕获;这意味着应用程序必须等待重新获得焦点才能退出。对我来说是不可接受的。 - static_rtti
经过一些思考,我认为我会实现Arthur Gaspar的最后一个解决方案,并且加入一个易于在调试时禁用它的系统。 - static_rtti

1

您可以使用标准的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 中,您可以释放所有资源(关闭所有数据库会话等),并优雅地关闭应用程序。

代码示例取自 此处


这并没有真正解决我的问题,因为我至少想在处理程序中掌握我的主窗口句柄。在你的例子中,你没有... - static_rtti
如果您将此代码放置在应用程序和主窗口创建之后,但在调用app._exec()之前,它确实会对您的应用程序和主窗口进行处理。 - Mr. B

1
你可以借助于 matplotlib 的解决方案。
matplotlib 中有一个名为 _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版本中可能会发生更改或消失,因此这更多地是一种“快速而脏”的解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接