Comment gérer l'événement de fermeture de fenêtre dans Tkinter?

131

Comment gérer l'événement de fermeture de fenêtre (l'utilisateur clique sur le bouton «X») dans un programme Python Tkinter?

Matt Gregory
la source

Réponses:

178

Tkinter prend en charge un mécanisme appelé gestionnaires de protocole . Ici, le terme protocole fait référence à l'interaction entre l'application et le gestionnaire de fenêtres. Le protocole le plus couramment utilisé est appelé WM_DELETE_WINDOWet sert à définir ce qui se passe lorsque l'utilisateur ferme explicitement une fenêtre à l'aide du gestionnaire de fenêtres.

Vous pouvez utiliser la protocolméthode pour installer un gestionnaire pour ce protocole (le widget doit être un widget Tkou Toplevel):

Voici un exemple concret:

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
la source
2
Si vous utilisez quelque chose comme Twisted qui maintient une boucle d'événement indépendamment ou Tkinter (par exemple: l'objet réacteur de twisted), assurez-vous que la boucle principale externe est arrêtée avec tout smenatics qu'elle fournit à cet effet (par exemple: reacteur.stop () pour twisted)
Brian Jack
4
Sur mon Python 2.7 sous Windows, Tkinterje n'avais pas de boîte de message de sous-module. J'ai utiliséimport tkMessageBox as messagebox
IronManMark20
Je pense que vous devriez à faire savoir que vous avez copié cette réponse et le code de quelqu'un / ailleurs.
Christian Dean
1
Je ne sais pas, ce n'est pas le code que j'ai initialement publié.
Matt Gregory
Ça ne marche pas pour moi. Cela ne change pas la réaction chaotique de Python classique à l'interruption des graphiques quand on ferme la fenêtre (par exemple avec Alt + F4).
Apostolos
29

Matt a montré une modification classique du bouton de fermeture.
L'autre est que le bouton de fermeture minimise la fenêtre.
Vous pouvez reproduire ce comportement en ayant la méthode iconify
comme deuxième argument de la méthode de protocole .

Voici un exemple fonctionnel, testé sur Windows 7 et 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()

Dans cet exemple, nous donnons à l'utilisateur deux nouvelles options de sortie:
le classique Fichier → Quitter, ainsi que le Escbouton.

Honnête Abe
la source
14

En fonction de l'activité de Tkinter, et en particulier lors de l'utilisation de Tkinter.after, arrêter cette activité avec destroy()- même en utilisant protocol (), un bouton, etc. - perturbera cette activité (erreur "lors de l'exécution") plutôt que de simplement la terminer . La meilleure solution dans presque tous les cas est d'utiliser un drapeau. Voici un exemple simple et idiot de comment l'utiliser (même si je suis certain que la plupart d'entre vous n'en ont pas besoin! :)

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

Cela termine bien l'activité graphique. Il vous suffit de vérifier runningau (x) bon (s) endroit (s).

Apostolos
la source
4

Je tiens à remercier la réponse d'Apostolos d'avoir porté cela à mon attention. Voici un exemple beaucoup plus détaillé pour Python 3 en 2019, avec une description plus claire et un exemple de code.


Méfiez-vous du fait que destroy()(ou ne pas avoir de gestionnaire de fermeture de fenêtre personnalisé du tout) détruira la fenêtre et tous ses rappels en cours d'exécution instantanément lorsque l'utilisateur la ferme.

Cela peut être mauvais pour vous, en fonction de votre activité Tkinter actuelle, et en particulier lors de l'utilisation tkinter.after(rappels périodiques). Vous pourriez utiliser un callback qui traite certaines données et écrit sur le disque ... dans ce cas, vous voulez évidemment que l'écriture des données se termine sans être brusquement tuée.

La meilleure solution pour cela est d'utiliser un drapeau. Ainsi, lorsque l'utilisateur demande la fermeture de la fenêtre, vous marquez cela comme un indicateur, puis vous y réagissez.

(Remarque: je conçois normalement les interfaces graphiques comme des classes bien encapsulées et des threads de travail séparés, et je n'utilise certainement pas "global" (j'utilise des variables d'instance de classe à la place), mais cela est censé être un exemple simple et dépouillé pour démontrer comment Tk tue brusquement vos rappels périodiques lorsque l'utilisateur ferme la fenêtre ...)

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

Ce code vous montrera que le WM_DELETE_WINDOWgestionnaire s'exécute même lorsque notre custom periodic_call()est occupé au milieu du travail / des boucles!

Nous utilisons des .after()valeurs assez exagérées : 500 millisecondes. Ceci est simplement destiné à vous permettre de voir très facilement la différence entre la fermeture pendant que l'appel périodique est occupé, ou non ... Si vous fermez pendant que les numéros sont mis à jour, vous verrez que WM_DELETE_WINDOWcela s'est produit pendant votre appel périodique "était traitement occupé: Vrai ". Si vous fermez pendant que les numéros sont en pause (ce qui signifie que le rappel périodique n'est pas en cours de traitement à ce moment-là), vous voyez que la fermeture s'est produite alors qu'il n'est "pas occupé".

Dans le monde réel, vous .after()utiliseriez quelque chose comme 30 à 100 millisecondes pour avoir une interface graphique réactive. Ceci est juste une démonstration pour vous aider à comprendre comment vous protéger contre le comportement par défaut de Tk "interrompre instantanément tout travail lors de la fermeture".

En résumé: faites en WM_DELETE_WINDOWsorte que le gestionnaire définisse un indicateur, puis vérifiez cet indicateur périodiquement et manuellement dans .destroy()la fenêtre lorsqu'elle est en sécurité (lorsque votre application a terminé son travail).

PS: Vous pouvez également utiliser WM_DELETE_WINDOWpour demander à l'utilisateur s'il veut VRAIMENT fermer la fenêtre; et s'ils répondent non, vous ne définissez pas le drapeau. C'est très simple. Il vous suffit d'afficher une boîte de message dans votre WM_DELETE_WINDOWet de définir l'indicateur en fonction de la réponse de l'utilisateur.

Mitch McMabers
la source
1

Essayez la version simple:

import tkinter

window = Tk()

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

window.mainloop()

Ou si vous souhaitez ajouter plus de commandes:

import tkinter

window = Tk()


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


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

window.mainloop()
Étude SF12
la source
La question concerne le bouton X du système d'exploitation pour fermer la fenêtre, et non un contrôle de bouton normal.
user1318499
-1
from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()
Tirth Anand
la source