2011-02-08 56 views
56

¿Cuál es la forma correcta de cerrar mi aplicación PyQt cuando muere desde la consola (Ctrl-C)?¿Cuál es la forma correcta de cerrar mi aplicación PyQt cuando muere desde la consola (Ctrl-C)?

Actualmente (no he hecho nada especial para manejar las señales de Unix), mi aplicación PyQt ignora SIGINT (Ctrl + C). Quiero que se comporte bien y renuncie cuando muera. ¿Cómo debo hacer eso?

+5

nunca he entendido por qué casi todos los script en Python en el mundo se detiene con un control + c a excepción de las aplicaciones pyqt. Sin duda hay una buena razón para eso, pero al final es muy molesto. – tokland

+0

@tokland: resolvamos esto una vez para siempre :) –

+1

parece ser un problema de diseño: http://www.mail-archive.com/[email protected]/msg13757.html. Cualquier solución que implique excepciones o similar solo se siente hacky :-( – tokland

Respuesta

38

17.4. signal — Set handlers for asynchronous events

Although Python signal handlers are called asynchronously as far as the Python user is concerned, they can only occur between the “atomic” instructions of the Python interpreter. This means that signals arriving during long calculations implemented purely in C (such as regular expression matches on large bodies of text) may be delayed for an arbitrary amount of time.

Eso significa Python no puede manejar señales mientras que el bucle de eventos de Qt está en marcha. Solo cuando se ejecuta el intérprete de Python (cuando se cierra QApplication o cuando se llama a una función de Python desde Qt) se llamará al controlador de señal.

Una solución es utilizar un QTimer para permitir que el intérprete se ejecute de vez en cuando.

Tenga en cuenta que, en el código siguiente, si no hay ventanas abiertas, la aplicación se cerrará después del cuadro de mensaje independientemente de la elección del usuario porque QApplication.quitOnLastWindowClosed() == True. Este comportamiento puede ser cambiado.

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

Otra posible solución, as pointed by LinearOrbit, es signal.signal(signal.SIGINT, signal.SIG_DFL), pero no permite que los controladores personalizados.

+0

Parece que no funciona ... Qt parece detectar la excepción antes que yo. –

+2

Su segunda solución funciona ... un poco. Cuando presiono Ctrl-C, la aplicación no termina inmediatamente como está esperado, pero espera hasta que se restablezca el foco en la aplicación. –

+0

De todos modos, gracias por su respuesta, intentaré hacer una pregunta más específica si nadie responde mejor –

2

Puede utilizar el pitón estándar de UNIX señales mecanismo de manejo:

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 

donde en signal_handler puede liberar todos los recursos (cerrar todas las sesiones db, etc) y cerrar suavemente su appliction.

ejemplo tomado del Código here

+0

Eso realmente no resuelve mi problema, porque me gustaría tener al menos un control sobre mi ventana principal en el controlador. En su ejemplo, usted no ... –

+0

Si coloca esto después de que se crean la aplicación y la ventana principal, pero antes de llamar a app._exec(), de hecho tiene un control sobre su aplicación y la ventana principal. –

2

creo que tengo una solución más simple:

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) 

Esto sólo indica a la aplicación para tratar de cerrar todas las ventanas si se pulsa Ctrl + C. Si hay un documento no guardado, su aplicación debería mostrar un cuadro de diálogo para guardar o cancelar como si hubiera salido.

Es posible que también necesite conectar la señal QApplication lastWindowClosed() a la salida de la ranura() para que la aplicación realmente salga cuando las ventanas estén cerradas.

+1

no he probado su solución, pero estoy bastante seguro de que sufre el mismo problema de una de las soluciones anteriores: –

+0

La señal no se detectará antes de que el control vuelva al intérprete de Python , es decir. antes de que la aplicación regrese de reposo; lo que significa que la aplicación tendrá que esperar para recuperar el foco para salir. Inaceptable para mí –

+0

Después de reflexionar, creo que implementaré la última solución de Arthur Gaspar, con un sistema para deshabilitarla fácilmente cuando se depure. –

34

Si simplemente desea tener Ctrl-C cerrar la aplicación - sin ser "agradable"/agraciado en ello - a continuación, a partir http://www.mail-archive.com/[email protected]/msg13758.html, puede utilizar esto:

import signal 
signal.signal(signal.SIGINT, signal.SIG_DFL) 

import sys 
from PyQt4.QtCore import QCoreApplication 
app = QCoreApplication(sys.argv) 
app.exec_() 

Al parecer, esto funciona en Linux, Windows y OSX: hasta ahora solo he probado esto en Linux (y funciona).

+3

Este trabajo, pero tenga en cuenta que omitirá cualquier limpieza que desee hacer, como las llamadas en los bloques finally. –

5

Encontré una manera de hacer esto. La idea es forzar qt para procesar eventos con la suficiente frecuencia y en un callabe de pitón para atrapar la señal 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_() 
+0

Lo conecté así para obtener un código de retorno también 'signal.signal (signal.SIGINT, lambda * a: app.exit (-2)) ' – dashesy

0

La respuesta de Artur Gaspar funcionó para mí cuando la ventana del terminal estaba enfocada, pero no funcionaba cuando la GUI estaba enfocada.Con el fin de conseguir mi interfaz gráfica de usuario para cerrar (que hereda de QWidget) tuviera que definir la siguiente función en la clase:

def keyPressEvent(self,event): 
    if event.key() == 67 and (event.modifiers() & QtCore.Qt.ControlModifier): 
     sigint_handler() 

comprobación para asegurarse de que la clave del evento es de 67 se asegura de que se ha pulsado 'c' . Luego, al verificar los modificadores de evento se determina si se presionó ctrl cuando se lanzó 'c'.

5

18.8.1.1. Execution of Python signal handlers

A Python signal handler does not get executed inside the low-level (C) signal handler. Instead, the low-level signal handler sets a flag which tells the virtual machine to execute the corresponding Python signal handler at a later point(for example at the next bytecode instruction). This has consequences:
[...]
A long-running calculation implemented purely in C (such as regular expression matching on a large body of text) may run uninterrupted for an arbitrary amount of time, regardless of any signals received. The Python signal handlers will be called when the calculation finishes.

El ciclo de eventos Qt se implementa en C (++). Eso significa que mientras se ejecuta y no se llama ningún código Python (por ejemplo, mediante una señal Qt conectada a una ranura Python), las señales se anotan, pero los manejadores de señal Python no son llamados.

Pero, ya que Python 2.6 y en Python 3 puede causar Qt para ejecutar una función de Python cuando una señal con un controlador se recibe mediante signal.set_wakeup_fd().

Esto es posible porque, contrariamente a la documentación, el manejador de señal de bajo nivel no solo establece un indicador para la máquina virtual, sino que también puede escribir un byte en el descriptor de archivo establecido en set_wakeup_fd(). Python 2 escribe un byte NUL, Python 3 escribe el número de señal.

Subclase una clase Qt que toma un descriptor de archivo y proporciona una señal readReady(), como p. Ej. QAbstractSocket, el bucle de eventos se ejecutará una función de Python cada vez que una señal (con un controlador) está recibida haciendo que el manejador de señales para ejecutar casi instantánea sin necesidad de temporizadores:

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

Desafortunadamente, esto no funciona en Windows ya que no hay' socket.socketpair'. (Intenté 'backports.socketpair', pero esto tampoco funciona). – coldfix

+0

En los conectores de Windows y otros controladores similares a archivos parece que se manejan por separado, por lo que probablemente necesite otra construcción que no use sockets. No uso Windows, así que no puedo probar lo que funcionaría. A partir de Python 3.5 parecen compatibles (consulte https://docs.python.org/3/library/signal.html#signal.set_wakeup_fd). – cg909

+0

Obtengo 'ValueError: el fd 10 debe estar en modo no bloqueado 'al intentar esto con Python 3.5.3 en macOS. –

Cuestiones relacionadas