¿Cómo manejo el evento de cierre de ventana en Tkinter?

131

¿Cómo manejo el evento de cierre de ventana (el usuario hace clic en el botón 'X') en un programa Python Tkinter?

Matt Gregory
fuente

Respuestas:

178

Tkinter admite un mecanismo llamado controladores de protocolo . Aquí, el término protocolo se refiere a la interacción entre la aplicación y el administrador de ventanas. Se llama al protocolo más utilizado WM_DELETE_WINDOWy se utiliza para definir qué sucede cuando el usuario cierra explícitamente una ventana utilizando el administrador de ventanas.

Puede usar el protocolmétodo para instalar un controlador para este protocolo (el widget debe ser un widget Tko Toplevel):

Aquí tienes un ejemplo concreto:

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
fuente
2
Si está utilizando algo como Twisted que mantiene un bucle de evento de forma independiente o Tkinter (por ejemplo, el objeto del reactor de twisted), asegúrese de que el bucle principal externo se detenga con cualquier smenático que proporcione para ese propósito (por ejemplo: reactor.stop () para twisted)
Brian Jack
44
En mi Python 2.7 en Windows, Tkinterno tenía un cuadro de mensaje de submódulo. Yo solíaimport tkMessageBox as messagebox
IronManMark20
Creo que debería dar a conocer que copió esta respuesta y el código de otra persona / lugar.
Christian Dean
1
No sé, ese no es el código que publiqué originalmente.
Matt Gregory
No funciona para mi No cambia la reacción caótica clásica de Python a la interrupción de los gráficos cuando uno cierra la ventana (por ejemplo, con Alt + F4).
Apostolos el
29

Matt ha mostrado una modificación clásica del botón de cierre.
El otro es hacer que el botón de cerrar minimice la ventana.
Puede reproducir este comportamiento haciendo que el método iconify
sea ​​el segundo argumento del método de protocolo .

Aquí hay un ejemplo de trabajo, probado en Windows 7 y 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()

En este ejemplo, le damos al usuario dos nuevas opciones de salida:
el clásico Archivo → Salir, y también el Escbotón.

Abe honesto
fuente
14

Dependiendo de la actividad de Tkinter, y especialmente cuando se usa Tkinter. Después, detener esta actividad con destroy()- incluso usando el protocolo (), un botón, etc. - perturbará esta actividad (error "al ejecutar") en lugar de simplemente terminarla . La mejor solución en casi todos los casos es usar una bandera. Aquí hay un ejemplo simple y tonto de cómo usarlo (¡aunque estoy seguro de que la mayoría de ustedes no lo necesita! :)

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

Esto termina la actividad gráfica muy bien. Solo necesita marcar runningen los lugares correctos.

Apostolos
fuente
4

Me gustaría agradecer la respuesta de Apostolos por llamar mi atención. Aquí hay un ejemplo mucho más detallado para Python 3 en el año 2019, con una descripción más clara y un código de ejemplo.


Tenga en cuenta el hecho de que destroy()(o no tener un controlador de cierre de ventana personalizado) destruirá la ventana y todas sus devoluciones de llamada en ejecución instantáneamente cuando el usuario la cierre.

Esto puede ser malo para usted, dependiendo de su actividad actual de Tkinter, y especialmente cuando usa tkinter.after(devoluciones de llamada periódicas). Es posible que esté utilizando una devolución de llamada que procesa algunos datos y escribe en el disco ... en ese caso, obviamente desea que finalice la escritura de datos sin que se elimine abruptamente.

La mejor solución para eso es usar una bandera. Entonces, cuando el usuario solicita el cierre de la ventana, lo marca como un indicador y luego reacciona a él.

(Nota: normalmente diseño interfaces gráficas de usuario como clases bien encapsuladas y subprocesos de trabajo separados, y definitivamente no uso "global" (en su lugar utilizo variables de instancia de clase), pero esto pretende ser un ejemplo simple y simplificado para demostrar cómo Tk mata abruptamente sus devoluciones de llamadas periódicas cuando el usuario cierra la ventana ...)

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

¡Este código le mostrará que el WM_DELETE_WINDOWcontrolador se ejecuta incluso mientras nuestra costumbre periodic_call()está ocupada en medio del trabajo / bucles!

Usamos algunos .after()valores bastante exagerados : 500 milisegundos. Esto solo tiene la intención de que sea muy fácil para usted ver la diferencia entre cerrar mientras la llamada periódica está ocupada, o no ... Si cierra mientras se actualizan los números, verá que WM_DELETE_WINDOWsucedió mientras su llamada periódica "estaba procesamiento ocupado: verdadero ". Si cierra mientras los números están en pausa (lo que significa que la devolución de llamada periódica no se está procesando en ese momento), verá que el cierre se produjo mientras "no está ocupado".

En el uso en el mundo real, .after()usaría algo así como 30-100 milisegundos, para tener una GUI receptiva. Esto es solo una demostración para ayudarlo a comprender cómo protegerse contra el comportamiento predeterminado de Tk de "interrumpir instantáneamente todo el trabajo al cerrar".

En resumen: haga que el WM_DELETE_WINDOWcontrolador establezca una bandera y luego verifique esa bandera periódicamente y manualmente en .destroy()la ventana cuando sea seguro (cuando su aplicación haya terminado con todo el trabajo).

PD: También puede usar WM_DELETE_WINDOWpara preguntar al usuario si REALMENTE desea cerrar la ventana; y si responden que no, no estableces la bandera. Es muy simple. Simplemente muestra un cuadro de mensaje en su WM_DELETE_WINDOWy establece el indicador en función de la respuesta del usuario.

Mitch McMabers
fuente
1

Prueba la versión simple:

import tkinter

window = Tk()

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

window.mainloop()

O si desea agregar más comandos:

import tkinter

window = Tk()


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


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

window.mainloop()
Estudio SF12
fuente
La pregunta es sobre el botón X del sistema operativo para cerrar la ventana, no un control de botón normal.
user1318499
-1
from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()
Tirth Anand
fuente