2011-02-08 9 views
56

Jaki jest poprawny sposób, aby moja aplikacja PyQt została zamknięta po zabiciu z konsoli (Ctrl-C)?Jaki jest poprawny sposób, aby moja aplikacja PyQt została zamknięta po zabiciu z konsoli (Ctrl-C)?

Obecnie (nie zrobiłem nic specjalnego dla obsługi sygnałów unix), moja aplikacja PyQt ignoruje SIGINT (Ctrl + C). Chcę, żeby ładnie się zachowywało i przestałem, kiedy zostało zabite. Jak mam to zrobić?

+5

Nigdy nie rozumiałem, dlaczego prawie każdy skrypt Pythona na świecie zatrzymuje się za pomocą kontrolki + c z wyjątkiem aplikacji pyqt. Bez wątpienia istnieje ku temu powód, ale ostatecznie jest to bardzo denerwujące. – tokland

+0

@tokland: niech rozwiązać ten problem raz na zawsze :) –

+1

to pojawia się problem design: http://www.mail-archive.com/[email protected]/msg13757.html. Każde rozwiązanie obejmujące wyjątki lub podobne odczucia są po prostu odrażające :-( – tokland

Odpowiedz

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.

Oznacza to, że uchwyt nie może Pythona sygnałów gdy pętla zdarzenie Qt jest uruchomiony. Dopiero po uruchomieniu interpretera Pythona (gdy QApplication zostanie wyłączony lub gdy wywoływana jest funkcja Python z Qt) zostanie wywołany moduł obsługi sygnału.

Rozwiązaniem jest użycie QTimeru, aby pozwolić interpreterowi działać od czasu do czasu.

Należy zauważyć, że w poniższym kodzie, jeśli nie ma otwartych okien, aplikacja zostanie zamknięta po oknie komunikatu, niezależnie od wyboru użytkownika, ponieważ QApplication.quitOnLastWindowClosed() == True. To zachowanie można zmienić.

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

Innym możliwym rozwiązaniem, as pointed by LinearOrbit jest signal.signal(signal.SIGINT, signal.SIG_DFL), ale nie pozwala na niestandardowe procedury obsługi.

+0

Wydaje się nie działać ... Qt wydaje się wychwycić wyjątek, zanim będę mógł –

+2

Twoje drugie rozwiązanie działa ... trochę. Kiedy naciśnie Ctrl-C, aplikacja nie kończy się natychmiast jak to oczekiwano, ale czeka, aż nacisk zostanie przywrócony do aplikacji. –

+0

w każdym razie, dzięki za odpowiedź, postaram się zadać bardziej konkretne pytanie, jeśli nikt nie daje lepszą odpowiedź. –

2

Można użyć standardowej Pythona Unix sygnały mechanizm rozpatrywania:

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 

gdzie w signal_handler można zwolnić wszystkich zasobów (zamknij wszystkie db sesje itp) i delikatnie zamknąć appliction.

przykład kodu pochodzących z here

+0

To naprawdę nie rozwiązuje mojego problemu, ponieważ chciałbym przynajmniej mieć uchwyt na moim główne okno w programie obsługi ... W twoim przykładzie nie ... –

+0

Jeśli umieścisz to po utworzeniu aplikacji i głównego okna, ale zanim zadzwonisz do app._exec(), rzeczywiście ma ona uchwyt w twojej aplikacji i główne okno –

2

Chyba mam prostsze rozwiązanie:

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) 

To właśnie mówi aplikację, aby spróbować, aby zamknąć wszystkie okna, jeśli ctrl + c jest wciśnięty. Jeśli jest niezapisany dokument, aplikacja powinna wyświetlić okno dialogowe zapisu lub anulowania, tak jakby zostało zakończone.

Konieczne może być również podłączenie sygnału QApplication lastWindowClosed() do zamknięcia gniazda(), aby aplikacja faktycznie opuściła okno po zamknięciu.

+1

nie testowałem swoje rozwiązanie, ale jestem pewien, że cierpi na tym samym problemem jednego roztworu powyżej: –

+0

sygnał nie zostanie złapany, zanim powróci do kontroli interpreter Pythona , tj. zanim aplikacja powróci z trybu uśpienia; co oznacza, że ​​aplikacja będzie musiała poczekać, aż odzyska fokus, aby wyjść. Niedopuszczalne dla mnie. –

+0

Po pewnym zastanowieniu, myślę, że wdrożę ostatnie rozwiązanie Arthura Gaspara, z systemem, który będzie go łatwo wyłączał podczas debugowania. –

34

Jeśli po prostu chcą mieć CTRL-C zamknąć aplikację - bez bycia „miły”/wdzięku o tym - potem od http://www.mail-archive.com/[email protected]/msg13758.html, można użyć tego:

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

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

Najwyraźniej działa na Linux, Windows i OSX - Do tej pory testowałem to tylko na Linuksie (i to działa).

+3

Ta praca, ale należy pamiętać, że obejdzie wszelkie czyszczenia, które chcesz zrobić, takie jak połączenia w końcu bloków. –

5

Znalazłem sposób na zrobienie tego. Chodzi o to, aby zmusić qt do częstego przetwarzania zdarzeń, a także w python callabe, aby złapać sygnał SIGINT.

+0

Połączyłem go w ten sposób, aby uzyskać również kod powrotu 'signal.signal (signal.SIGINT, lambda * a: app.exit (-2)) ' – dashesy

0

Odpowiedź od Artura Gaspara zadziałała dla mnie, gdy okno terminalu było ostre, ale nie działało, gdy interfejs GUI był ostry.Aby dostać mój GUI, aby zamknąć (która dziedziczy z QWidget) musiałem zdefiniować następującą funkcję w klasie:

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

Sprawdzanie aby upewnić się, że kluczowym wydarzeniem jest 67 zapewnia, że ​​„c” został naciśnięty . Następnie sprawdzenie modyfikatorów zdarzeń określa, czy naciśnięto klawisz Ctrl po zwolnieniu przycisku "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.

Pętla zdarzeń Qt jest zaimplementowana w C (++). Oznacza to, że podczas działania i bez wywoływania kodu Pythona (np. Przez sygnał Qt podłączony do szczeliny Pythona), sygnały są odnotowywane, ale procedury obsługi sygnałów w Pythonie nie są wywoływane.

Ale, ponieważ Python 2.6 w Pythonie i 3 może być przyczyną Qt uruchomić funkcję Pythona, gdy odbierany jest sygnał z obsługi za pomocą signal.set_wakeup_fd().

Jest to możliwe, ponieważ w przeciwieństwie do dokumentacji, obsługa sygnału niskiego poziomu nie tylko ustawia flagę dla maszyny wirtualnej, ale może również zapisywać bajt w deskryptorze pliku ustawionym przez set_wakeup_fd(). Python 2 zapisuje bajt NUL, Python 3 zapisuje numer sygnału.

Więc przez instacji klasy Qt, która pobiera deskryptor pliku i dostarcza sygnał readReady(), jak na przykład QAbstractSocket, pętla zdarzenie będzie wykonywał funkcję Pythona za każdym razem sygnał (z obsługi) jest odbierany powodując obsługi sygnału wykonać prawie natychmiast bez konieczności timerów:

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

Niestety, to nie działa w systemie Windows, ponieważ nie ma' socket.socketpair'. (Próbowałem 'backports.socketpair', ale to też nie działa). – coldfix

+0

W przypadku gniazd Windows i innych uchwytów podobnych do plików wydaje się, że są obsługiwane oddzielnie, więc prawdopodobnie potrzebujesz innej konstrukcji, która nie używa gniazd. Nie używam systemu Windows, więc nie mogę przetestować działania. Od Pythona 3.5 gniazda wydają się być obsługiwane (zobacz https://docs.python.org/3/library/signal.html#signal.set_wakeup_fd). – cg909

+0

Otrzymuję komunikat "ValueError: fd 10 musi być w trybie bez blokowania", gdy próbuję tego z Pythonem 3.5.3 na macOS. –