从控制台(Ctrl-C)终止时让我的 PyQt 应用程序退出的正确方法是啥?

Posted

技术标签:

【中文标题】从控制台(Ctrl-C)终止时让我的 PyQt 应用程序退出的正确方法是啥?【英文标题】:What is the correct way to make my PyQt application quit when killed from the console (Ctrl-C)?从控制台(Ctrl-C)终止时让我的 PyQt 应用程序退出的正确方法是什么? 【发布时间】:2011-06-23 17:58:15 【问题描述】:

什么是让我的 PyQt 应用程序在从控制台 (Ctrl-C) 终止时退出的正确方法?

目前(我没有做任何特殊的处理 unix 信号),我的 PyQt 应用程序忽略了 SIGINT (Ctrl+C)。我希望它表现得很好,并在它被杀死时退出。我该怎么做?

【问题讨论】:

我一直不明白为什么除了 pyqt 应用程序之外,世界上几乎所有的 python 脚本都以 control+c 停止。毫无疑问,这是有充分理由的,但到头来却很烦人。 @tokland : 让我们一次性解决这个问题:) 这似乎是一个设计问题:mail-archive.com/pyqt@riverbankcomputing.com/msg13757.html。任何涉及异常或类似情况的解决方案都会让人觉得很老套:-( 您可以使用 Ctrl + \ 从终端杀死应用程序。 【参考方案1】:

17.4. signal — Set handlers for asynchronous events

尽管就 Python 用户而言,Python 信号处理程序是异步调用的,但它们只能出现在 Python 解释器的“原子”指令之间。这意味着在纯 C 中实现的长时间计算期间到达的信号(例如对大量文本的正则表达式匹配)可能会延迟任意时间。

这意味着 Python 无法在 Qt 事件循环运行时处理信号。只有当 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_())

另一个可能的解决方案as pointed by LinearOrbit 是signal.signal(signal.SIGINT, signal.SIG_DFL),但它不允许自定义处理程序。

【讨论】:

似乎不起作用... Qt 似乎在我能之前就捕捉到了异常。 您的第二个解决方案有效......有点。当我按下 Ctrl-C 时,应用程序不会像预期的那样立即终止,而是等待焦点恢复到应用程序。 无论如何,谢谢你的回答,如果没有人给出更好的答案,我会尝试问一个更具体的问题。 谢谢!不过,这似乎有点矫枉过正:) 我想我会直接问 PyQt 的人,如果你的解决方案真的是唯一的:) PyQt 人员有什么回应吗?【参考方案2】:

如果你只是想让 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 上测试过它(并且它有效)。

【讨论】:

这项工作,但请注意,它会绕过您希望执行的任何清理操作,例如在 finally 块中调用。【参考方案3】:

18.8.1.1. Execution of Python signal handlers

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 写入信号编号。

因此,通过继承一个 Qt 类,该类接受一个文件描述符并提供一个readReady() 信号,例如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,但这也不起作用)。 在 Windows 上,套接字和其他类似文件的句柄似乎是分开处理的,因此您可能需要另一个不使用套接字的构造。我不使用 Windows,所以我无法测试什么会起作用。从 Python 3.5 开始,似乎支持套接字(请参阅 docs.python.org/3/library/signal.html#signal.set_wakeup_fd )。 在 macOS 上使用 Python 3.5.3 尝试此操作时,我收到了 ValueError: the fd 10 must be in non-blocking mode self.wsock.setblocking(False) 修复了我之前推荐中提到的问题。【参考方案4】:

我找到了一种方法来做到这一点。这个想法是强制 qt 足够频繁地处理事件,并在 python callabe 中捕获 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))【参考方案5】:

当终端窗口处于焦点时,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。

【讨论】:

【参考方案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

【讨论】:

这个和cg909's post一起是正确答案。【参考方案7】:

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

取自here的代码示例

【讨论】:

这并不能真正解决我的问题,因为我想至少在处理程序中的主窗口上有一个句柄。在您的示例中,您没有... 如果你在创建应用程序和主窗口之后,但在调用 app._exec() 之前放置它,它确实对你的应用程序和主窗口有句柄。【参考方案8】:

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

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解释器之前不会捕获信号,即。在应用程序从睡眠状态返回之前;这意味着应用程序必须等待重新获得焦点才能退出。我无法接受。 经过一番思考,我想我会实现 Arthur Gaspar 的最后一个解决方案,在调试时使用一个可以轻松禁用它的系统。

以上是关于从控制台(Ctrl-C)终止时让我的 PyQt 应用程序退出的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在发送 http 请求时让 PyQt5 标签动态更新

优雅地终止基于 Boost Asio 的 Windows 控制台应用程序

我可以在选择时让我的实体框架DbSet调用我的表值函数吗?

在 JAVA API 上使用时让我们加密证书不起作用

linux 进程 ctrl-c,ctrl-z,ctrl-d

PyQt5 QThread 不会因终止或标志变量而停止