169 votes

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

Comment puis-je gérer l'événement de fermeture de la fenêtre (l'utilisateur cliquant sur le bouton 'X') dans un programme Python Tkinter ?

242voto

Matt Gregory Points 978

Tkinter supporte un mécanisme appelé les 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_WINDOW et est utilisé pour définir ce qui se passe lorsque l'utilisateur ferme explicitement une fenêtre en utilisant le gestionnaire de fenêtres.

Vous pouvez utiliser le protocol pour installer un gestionnaire pour ce protocole (le widget doit être un Tk ou Toplevel widget) :

Vous avez ici 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()

2 votes

Si vous utilisez quelque chose comme Twisted qui maintient une boucle d'événements indépendamment de Tkinter (par exemple, l'objet reactor de Twisted), assurez-vous que la boucle principale externe est arrêtée avec n'importe quelle smenatique fournie à cet effet (par exemple, reactor.stop() pour Twisted).

5 votes

Sur mon Python 2.7 sous Windows, Tkinter n'avait pas de boîte à messages pour le sous-module. J'ai utilisé import tkMessageBox as messagebox

0 votes

Je pense que vous devriez faire savoir que vous avez copié cette réponse et ce code de quelqu'un ou d'un autre.

42voto

Honest Abe Points 2343

Matt a montré une modification classique du bouton de fermeture.
L'autre solution consiste à faire en sorte que le bouton de fermeture réduise la fenêtre.
Vous pouvez reproduire ce comportement en faisant en sorte que l'option iconifier méthode
être le protocole le deuxième argument de la méthode.

Voici un exemple fonctionnel, testé sous 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 File Exit, et aussi le Esc bouton.

3 votes

Tout à fait intéressant ! Cependant, je désinstallerais immédiatement un programme qui ne se ferme pas lorsque j'appuie sur le bouton de fermeture.

1 votes

Oui, ça viole le principe du moindre étonnement . Je vais le laisser, car il s'agit toujours d'une réponse valable à la question, et l'exemple a une valeur pédagogique supplémentaire.

1 votes

Absolument, j'ai donné mon avis favorable :-)

17voto

Apostolos Points 847

En fonction de l'activité 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 "while executing") plutôt que de la terminer. La meilleure solution dans presque tous les cas est d'utiliser un drapeau. Voici un exemple simple et stupide de son utilisation (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 met fin à l'activité graphique de façon agréable. Il suffit de vérifier running au(x) bon(s) endroit(s).

6voto

Mitch McMabers Points 1446

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


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

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

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

(Note : Je conçois normalement les interfaces graphiques comme des classes joliment encapsulées et des threads de travail séparés, et je n'utilise absolument pas "global" (j'utilise plutôt des variables d'instance de classe), mais ceci est censé être un exemple simple et dépouillé pour démontrer comment Tk tue brusquement vos callbacks 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_WINDOW s'exécute même si notre gestionnaire personnalisé periodic_call() est occupé au milieu du travail/des boucles !

Nous utilisons certains assez exagérés .after() valeurs : 500 millisecondes. Ceci est juste destiné à vous permettre de voir très facilement la différence entre fermer pendant que l'appel périodique est occupé, ou pas... Si vous fermez pendant que les chiffres sont en train de se mettre à jour, vous verrez que l'option WM_DELETE_WINDOW est arrivé tandis que votre appel périodique "était occupé à traiter : True". Si vous fermez alors 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 verrez que la fermeture a eu lieu alors que le système n'était pas occupé.

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

En résumé, faites le WM_DELETE_WINDOW met un drapeau, puis vérifie ce drapeau périodiquement et manuellement. .destroy() la fenêtre quand c'est sûr (quand votre application a terminé tout le travail).

PS : Vous pouvez également utiliser WM_DELETE_WINDOW à demandez à l'utilisateur s'il veut VRAIMENT fermer la fenêtre ; et s'il répond non, vous n'activez pas le drapeau. C'est très simple. Il suffit d'afficher une boîte de message dans votre WM_DELETE_WINDOW et mettre le drapeau en fonction de la réponse de l'utilisateur.

0voto

SF12 Study Points 106

Essayez la version simple :

import tkinter

window = Tk()

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

window.mainloop()

Ou si vous voulez ajouter d'autres commandes :

import tkinter

window = Tk()

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

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

window.mainloop()

0 votes

La question concerne le bouton X du système d'exploitation pour fermer la fenêtre, et non un bouton de commande ordinaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X