Jak obsłużyć zdarzenie zamknięcia okna w Tkinter?

130

Jak obsłużyć zdarzenie zamknięcia okna (użytkownik kliknie przycisk „X”) w programie Python Tkinter?

Matt Gregory
źródło

Odpowiedzi:

178

Tkinter obsługuje mechanizm zwany programami obsługi protokołów . Tutaj termin protokół odnosi się do interakcji między aplikacją a menedżerem okien. Najczęściej używany protokół jest nazywany WM_DELETE_WINDOWi służy do definiowania tego, co się dzieje, gdy użytkownik jawnie zamyka okno za pomocą menedżera okien.

Możesz użyć tej protocolmetody, aby zainstalować program obsługi dla tego protokołu (widget musi być widgetem Tklub Toplevel):

Tutaj masz konkretny przykład:

import tkinter as tk
from tkinter import messagebox

root = tk.Tk()

def on_closing():
    if messagebox.askokcancel("Quit", "Do you want to quit?"):
        root.destroy()

root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
Matt Gregory
źródło
2
Jeśli używasz czegoś takiego jak Twisted, które niezależnie utrzymuje pętlę zdarzeń lub Tkinter (np .: obiekt reaktora twisted), upewnij się, że zewnętrzna główna pętla jest zatrzymana za pomocą wszelkich smenatyków, które zapewnia do tego celu (np: reactor.stop () dla twisted)
Brian Jack
4
Na moim Pythonie 2.7 w systemie Windows Tkinternie miałem pola wiadomości podmodułu. Użyłemimport tkMessageBox as messagebox
IronManMark20
Myślę, że powinieneś powiadomić, że skopiowałeś tę odpowiedź i kod od kogoś / gdzie indziej.
Christian Dean
1
Nie wiem, to nie jest kod, który pierwotnie opublikowałem.
Matt Gregory
Nie działa na mnie. Nie zmienia to chaotycznej reakcji klasycznego Pythona na przerwanie grafiki, gdy mocno zamyka się okno (np. Za pomocą Alt + F4).
Apostolos
29

Matt pokazał jedną klasyczną modyfikację przycisku zamykającego.
Drugi to przycisk zamykania, który minimalizuje okno.
Możesz odtworzyć to zachowanie, mając metodę iconify
jako drugi argument metody protokołu .

Oto działający przykład, przetestowany na Windows 7 i 10:

# Python 3
import tkinter
import tkinter.scrolledtext as scrolledtext

root = tkinter.Tk()
# make the top right close button minimize (iconify) the main window
root.protocol("WM_DELETE_WINDOW", root.iconify)
# make Esc exit the program
root.bind('<Escape>', lambda e: root.destroy())

# create a menu bar with an Exit command
menubar = tkinter.Menu(root)
filemenu = tkinter.Menu(menubar, tearoff=0)
filemenu.add_command(label="Exit", command=root.destroy)
menubar.add_cascade(label="File", menu=filemenu)
root.config(menu=menubar)

# create a Text widget with a Scrollbar attached
txt = scrolledtext.ScrolledText(root, undo=True)
txt['font'] = ('consolas', '12')
txt.pack(expand=True, fill='both')

root.mainloop()

W tym przykładzie dajemy użytkownikowi dwie nowe opcje wyjścia:
klasyczną Plik → Zakończ, a także Escprzycisk.

Szczery Abe
źródło
12

W zależności od aktywności Tkinter, a zwłaszcza podczas korzystania z Tkinter.after, zatrzymanie tej czynności za pomocą destroy()- nawet przy użyciu protokołu (), przycisku itp. - zakłóci tę czynność (błąd „podczas wykonywania”), a nie po prostu ją zakończy . Prawie w każdym przypadku najlepszym rozwiązaniem jest użycie flagi. Oto prosty, głupiutki przykład, jak go używać (chociaż jestem pewien, że większość z Was go nie potrzebuje! :)

from Tkinter import *

def close_window():
  global running
  running = False  # turn off while loop
  print( "Window closed")

root = Tk()
root.protocol("WM_DELETE_WINDOW", close_window)
cv = Canvas(root, width=200, height=200)
cv.pack()

running = True;
# This is an endless loop stopped only by setting 'running' to 'False'
while running: 
  for i in range(200): 
    if not running: 
        break
    cv.create_oval(i, i, i+1, i+1)
    root.update() 

To ładnie kończy aktywność graficzną. Musisz tylko sprawdzić runningwe właściwym miejscu (miejscach).

Apostolos
źródło
4

Chciałbym podziękować odpowiedzi Apostolos za zwrócenie mi na to uwagi. Oto znacznie bardziej szczegółowy przykład dla Pythona 3 w roku 2019, z jaśniejszym opisem i przykładowym kodem.


Uważaj na fakt, że destroy()(lub w ogóle brak niestandardowego modułu obsługi zamykania okna) zniszczy okno i wszystkie jego uruchomione wywołania zwrotne natychmiast po zamknięciu go przez użytkownika.

Może to być dla Ciebie złe, w zależności od Twojej bieżącej aktywności Tkinter, a zwłaszcza podczas korzystania z tkinter.after(okresowe wywołania zwrotne). Być może używasz wywołania zwrotnego, które przetwarza niektóre dane i zapisuje na dysku ... w takim przypadku chcesz oczywiście zakończyć zapisywanie danych bez nagłego zabicia.

Najlepszym rozwiązaniem jest użycie flagi. Więc kiedy użytkownik zażąda zamknięcia okna, zaznaczasz to jako flagę, a następnie reagujesz na to.

(Uwaga: zwykle projektuję GUI jako ładnie zamknięte klasy i oddzielne wątki robocze i zdecydowanie nie używam „globalnego” (zamiast tego używam zmiennych instancji klas), ale ma to być prosty, okrojony przykład pokazujący jak Tk nagle zabija twoje okresowe wywołania zwrotne, gdy użytkownik zamyka okno ...)

from tkinter import *
import time

# Try setting this to False and look at the printed numbers (1 to 10)
# during the work-loop, if you close the window while the periodic_call
# worker is busy working (printing). It will abruptly end the numbers,
# and kill the periodic callback! That's why you should design most
# applications with a safe closing callback as described in this demo.
safe_closing = True

# ---------

busy_processing = False
close_requested = False

def close_window():
    global close_requested
    close_requested = True
    print("User requested close at:", time.time(), "Was busy processing:", busy_processing)

root = Tk()
if safe_closing:
    root.protocol("WM_DELETE_WINDOW", close_window)
lbl = Label(root)
lbl.pack()

def periodic_call():
    global busy_processing

    if not close_requested:
        busy_processing = True
        for i in range(10):
            print((i+1), "of 10")
            time.sleep(0.2)
            lbl["text"] = str(time.time()) # Will error if force-closed.
            root.update() # Force redrawing since we change label multiple times in a row.
        busy_processing = False
        root.after(500, periodic_call)
    else:
        print("Destroying GUI at:", time.time())
        try: # "destroy()" can throw, so you should wrap it like this.
            root.destroy()
        except:
            # NOTE: In most code, you'll wanna force a close here via
            # "exit" if the window failed to destroy. Just ensure that
            # you have no code after your `mainloop()` call (at the
            # bottom of this file), since the exit call will cause the
            # process to terminate immediately without running any more
            # code. Of course, you should NEVER have code after your
            # `mainloop()` call in well-designed code anyway...
            # exit(0)
            pass

root.after_idle(periodic_call)
root.mainloop()

Ten kod pokaże ci, że program WM_DELETE_WINDOWobsługi działa nawet wtedy, gdy nasz niestandardowy periodic_call()jest zajęty w środku pracy / pętli!

Używamy dość przesadzonych .after()wartości: 500 milisekund. Ma to tylko na celu ułatwienie Ci dostrzeżenia różnicy między zamknięciem, gdy okresowa rozmowa jest zajęta, czy nie… Jeśli zamkniesz konto podczas aktualizacji numerów, zobaczysz, że WM_DELETE_WINDOWstało się to, gdy Twoje połączenie okresowe było zajęte przetwarzanie: prawda ”. Jeśli zamkniesz, gdy numery są wstrzymane (co oznacza, że ​​okresowe oddzwanianie nie jest w tej chwili przetwarzane), zobaczysz, że zamknięcie nastąpiło, gdy nie jest zajęty.

W prawdziwym świecie .after()potrzebowałbyś około 30-100 milisekund, aby mieć responsywny GUI. To tylko demonstracja, która pomoże ci zrozumieć, jak chronić się przed domyślnym zachowaniem Tk „natychmiast przerywaj całą pracę przy zamykaniu”.

Podsumowując: spraw, aby program WM_DELETE_WINDOWobsługi ustawił flagę, a następnie okresowo sprawdzaj tę flagę i ręcznie sprawdzaj .destroy()okno, gdy jest to bezpieczne (gdy aplikacja jest zakończona z całą pracą).

PS: Możesz również użyć, WM_DELETE_WINDOWaby zapytać użytkownika, czy NAPRAWDĘ chce zamknąć okno; a jeśli odpowiedzą „nie”, nie ustawiasz flagi. To jest bardzo proste. Po prostu pokazujesz skrzynkę wiadomości w swoim WM_DELETE_WINDOWi ustawiasz flagę na podstawie odpowiedzi użytkownika.

Mitch McMabers
źródło
1

Wypróbuj prostą wersję:

import tkinter

window = Tk()

closebutton = Button(window, text='X', command=window.destroy)
closebutton.pack()

window.mainloop()

Lub jeśli chcesz dodać więcej poleceń:

import tkinter

window = Tk()


def close():
    window.destroy()
    #More Functions


closebutton = Button(window, text='X', command=close)
closebutton.pack()

window.mainloop()
Badanie SF12
źródło
Pytanie dotyczy przycisku X systemu operacyjnego do zamykania okna, a nie zwykłego przycisku sterującego.
user1318499
-1
from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()
Tirth Anand
źródło