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


70

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

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


7
我从来不明白为什么世界上几乎所有的python脚本都用Ctrl + C停止,而pyqt应用程序除外。毫无疑问,这是有充分理由的,但是最后这很烦人。
tokland 2011年

1
@tokland:让我们一次解决所有问题:)
static_rtti 2011年

3
它似乎是一个设计问题:mail-archive.com/pyqt@riverbankcomputing.com/msg13757.html。任何涉及异常或类似问题的解决方案都感觉很hacky :-(
tokland 2011年

4
您可以使用Ctrl + \从终端中终止该应用程序。
2013年

Answers:


50

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


1
似乎不起作用... Qt似乎在我能赶上之前就捕获了异常。
static_rtti 2011年

3
您的第二个解决方案有效。当我按Ctrl-C组合键时,应用程序不会像预期的那样立即终止,但是要等到焦点恢复到应用程序为止。
static_rtti 2011年

无论如何,感谢您的回答,如果没有人提供更好的答案,我会尝试提出更具体的问题。
static_rtti 2011年

1
谢谢!不过,似乎有点
矫kill过

PyQt员工有何回应?
Michael Clerx 2013年

40

如果您只是希望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上进行过测试(并且可以使用)。


3
这项工作,但请注意,它将绕过您希望执行的任何清理工作,例如在finally块中进行调用。
e-satis

7

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

Python信号处理程序不会在底层(C)信号处理程序中执行。相反,低级信号处理程序设置一个标志,该标志告诉虚拟机在稍后的位置(例如,在下一个字节码指令处)执行相应的Python信号处理程序。这具有的后果:
[...]
长期运行的计算实现纯粹在C(例如在大体文本的正则表达式匹配)可以为任意的时间量不间断地运行,而不管接收到的任何信号。计算完成后,将调用Python信号处理程序。

Qt事件循环是用C(++)实现的。这意味着,尽管它运行并且没有调用任何Python代码(例如,通过连接到Python插槽的Qt信号),但信号被记录下来,但未调用Python信号处理程序。

但是,从Python 2.6和Python 3开始,当您使用接收到带有处理程序的信号时,可以使Qt运行Python函数signal.set_wakeup_fd()

这是有可能的,因为与文档相反,低级信号处理程序不仅为虚拟机设置了一个标志,而且还可以将字节写入由设置的文件描述符中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 '16

在Windows上,套接字和其他类似文件的句柄似乎是分开处理的,因此您可能需要另一个不使用套接字的构造。我没有使用Windows,所以我无法测试将如何工作。从Python 3.5开始,似乎支持套接字(请参阅docs.python.org/3/library/signal.html#signal.set_wakeup_fd)。
cg909 '16

ValueError: the fd 10 must be in non-blocking mode在macOS上使用Python 3.5.3尝试时遇到问题。
Michael Herrmann

1
self.wsock.setblocking(False)解决了我先前的推荐中提到的问题。
Michael Herrmann

6

我找到了一种方法。这个想法是迫使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))
破旧的

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

您可以使用标准的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 2011年

如果将其放置在创建应用程序和主窗口之后,但在调用app._exec()之前,则确实在应用程序和主窗口上具有一个句柄。
B先生

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 2011年

在控制权返回到python解释器之前,即不会捕获信号。在应用程序从睡眠返回之前;这意味着应用程序将不得不等待重新获得焦点才能退出。对我来说是无法接受的。
static_rtti 2011年

经过一番思考,我想我将实现Arthur Gaspar的最后一个解决方案,并提供一个系统,该系统可以在调试时轻松禁用它。
static_rtti 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.