Como lidar com o evento de fechamento de janela no Tkinter?

131

Como lidar com o evento de fechamento da janela (usuário clicando no botão 'X') em um programa Python Tkinter?

Matt Gregory
fonte

Respostas:

178

O Tkinter suporta um mecanismo chamado manipuladores de protocolo . Aqui, o termo protocolo refere-se à interação entre o aplicativo e o gerenciador de janelas. O protocolo mais usado é chamadoWM_DELETE_WINDOW e é usado para definir o que acontece quando o usuário fecha explicitamente uma janela usando o gerenciador de janelas.

Você pode usar o protocolmétodo para instalar um manipulador para este protocolo (o widget deve ser um Tkou Toplevelwidget):

Aqui você tem um exemplo 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
fonte
2
Se você estiver usando algo como Twisted, que mantém um loop de eventos independentemente ou Tkinter (por exemplo: objeto do reator de twisted), verifique se o loop principal externo está parado com as smenatics que ele fornece para esse fim (por exemplo: reattor.stop () para twisted)
Brian Jack
4
No meu Python 2.7 no Windows, Tkinternão havia uma caixa de mensagens do submódulo. Eu useiimport tkMessageBox as messagebox
IronManMark20
Eu acho que você deveria saber que copiou esta resposta e código de alguém / onde mais.
Christian Dean
1
Não sei, esse não é o código que publiquei originalmente.
Matt Gregory
Não funciona para mim. Não muda a reação caótica do Python clássico à interrupção de gráficos quando alguém fecha a janela com força (por exemplo, com Alt + F4).
Apostolos
29

Matt mostrou uma modificação clássica do botão Fechar.
A outra é fazer com que o botão fechar minimize a janela.
Você pode reproduzir esse comportamento fazendo com que o método iconify
seja o segundo argumento do método de protocolo .

Aqui está um exemplo de trabalho, testado no Windows 7 e 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()

Neste exemplo, damos ao usuário duas novas opções de saída:
o arquivo clássico → Sair e também o Escbotão.

Honesto Abe
fonte
14

Dependendo da atividade do Tkinter, e especialmente ao usar o Tkinter.after, interromper essa atividade com destroy()- mesmo usando o protocolo (), um botão etc. - perturbará essa atividade (erro "durante a execução") em vez de apenas encerrá-la . A melhor solução em quase todos os casos é usar uma bandeira. Aqui está um exemplo simples e bobo de como usá-lo (embora eu esteja certo de que a maioria de vocês não precisa!)

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

Isso finaliza bem a atividade gráfica. Você só precisa verificar runningno (s) local (is) certo (s).

Apostolos
fonte
4

Gostaria de agradecer a resposta de Apostolos por trazer isso à minha atenção. Aqui está um exemplo muito mais detalhado do Python 3 no ano de 2019, com uma descrição e um código de exemplo mais claros.


Cuidado com o fato de que destroy()(ou não ter um manipulador de fechamento de janela personalizado) destruirá a janela e todos os seus retornos de chamada em execução instantaneamente quando o usuário a fechar.

Isso pode ser ruim para você, dependendo da atividade atual do Tkinter e, principalmente, ao usar tkinter.after (retornos de chamada periódicos). Você pode estar usando um retorno de chamada que processa alguns dados e grava no disco ... nesse caso, obviamente você deseja que a gravação dos dados termine sem ser interrompida abruptamente.

A melhor solução para isso é usar uma bandeira. Portanto, quando o usuário solicitar o fechamento da janela, marque-o como um sinalizador e reaja a ele.

(Nota: eu normalmente projeto GUIs como classes bem encapsuladas e segmentos de trabalho separados, e definitivamente não uso "global" (eu uso variáveis ​​de instância de classe), mas isso deve ser um exemplo simples e simples para demonstrar como Tk mata abruptamente seus retornos de chamada periódicos quando o usuário fecha a janela ...)

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

Esse código mostra que o WM_DELETE_WINDOWmanipulador é executado mesmo enquanto nossoperiodic_call() está ocupado no meio do trabalho / loops!

Usamos alguns .after()valores bastante exagerados : 500 milissegundos. Isso serve apenas para facilitar a visualização da diferença entre o fechamento enquanto a chamada periódica está ocupada ou não ... Se você fechar enquanto os números estiverem sendo atualizados, verá que WM_DELETE_WINDOWaconteceu enquanto a ligação periódica "foi processamento ocupado: True ". Se você fechar enquanto os números estiverem pausados ​​(o que significa que o retorno periódico de chamada não está sendo processado naquele momento), você verá que o fechamento ocorreu enquanto "não está ocupado".

No uso no mundo real, seu .after() usaria algo como 30 a 100 milissegundos, para ter uma GUI responsiva. Esta é apenas uma demonstração para ajudá-lo a entender como se proteger do comportamento padrão de Tk "interrompe instantaneamente todo o trabalho ao fechar" o comportamento.

Em resumo: faça com que o WM_DELETE_WINDOWmanipulador defina um sinalizador e verifique periodicamente e manualmente .destroy()a janela quando estiver seguro (quando seu aplicativo terminar com todo o trabalho).

PS: Você também pode usar WM_DELETE_WINDOWpara perguntar ao usuário se eles realmente querem fechar a janela; e se eles responderem não, você não definirá a bandeira. É muito simples. Você apenas mostra uma caixa de mensagens na sua WM_DELETE_WINDOWe define a sinalização com base na resposta do usuário.

Mitch McMabers
fonte
1

Experimente a versão simples:

import tkinter

window = Tk()

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

window.mainloop()

Ou se você deseja adicionar mais comandos:

import tkinter

window = Tk()


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


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

window.mainloop()
Estudo SF12
fonte
A pergunta é sobre o botão X do sistema operacional para fechar a janela, não um controle regular de botão.
user1318499 26/03
-1
from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()
Tirth Anand
fonte