当从控制台(Ctrl-C)关闭 PyQt 应用程序时,使其退出的正确方法是什么?

当从控制台(Ctrl-C)关闭 PyQt 应用程序时,使其退出的正确方法是什么?

目前(我没有做任何处理 unix 信号的特殊工作) ,PyQt 应用程序忽略 SIGINT (Ctrl + C)。我希望它乖乖的,等它被杀了就放弃。我该怎么做?

49235 次浏览

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

取自 给你的代码示例

信号ーー为异步事件设置处理程序

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

这意味着 Python 不能在 Qt 事件循环运行时处理信号。只有当 Python 解释器运行时(QApplication 退出时,或者从 Qt 调用 Python 函数时) ,才会调用信号处理程序。

一个解决方案是使用 QTimer 让解释器时不时地运行。

注意,在下面的代码中,如果没有打开的窗口,应用程序将在消息框之后退出,而不管用户的选择是什么,因为 QApplication.quitOnLastWindowClose () = = 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_())

另一个可能的解决方案 正如线性轨道所指出的signal.signal(signal.SIGINT, signal.SIG_DFL),但它不允许自定义处理程序。

我想我有一个更简单的解决办法:

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 信号 lastWindowClose ()连接到插槽 exit () ,以使应用程序在窗口关闭时实际退出。

如果您只是希望 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 上测试过(而且它也可以工作)。

我找到办法了。其思想是强制 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_()

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。

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

因此,通过子类化一个 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_())

Cg909/Michael Herrmann 提出的异步方法对于替换计时器来说非常有趣。因此,这里有一个简化版本,它也使用 socket.socketdouble (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

你可以利用 matplotlib 的解决方案。

Matplotlib 在 matplotlib.backends.backend_qt中隐藏了一个名为 _maybe_allow_interrupt的函数

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 的未来版本中它可能会更改或消失,所以这更像是一个“快速而肮脏”的解决方案。