如何在Tkinter中处理窗口关闭事件?

问题:如何在Tkinter中处理窗口关闭事件?

如何在Python Tkinter程序中处理窗口关闭事件(用户单击“ X”按钮)?

How do I handle the window close event (user clicking the ‘X’ button) in a Python Tkinter program?


回答 0

Tkinter支持一种称为协议处理程序的机制。在这里,术语协议是指应用程序和窗口管理器之间的交互。最常用的协议称为WM_DELETE_WINDOW,用于定义当用户使用窗口管理器显式关闭窗口时发生的情况。

您可以使用该protocol方法为该协议安装处理程序(窗口小部件必须是TkToplevel窗口小部件):

这里有一个具体的例子:

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

Tkinter supports a mechanism called protocol handlers. Here, the term protocol refers to the interaction between the application and the window manager. The most commonly used protocol is called WM_DELETE_WINDOW, and is used to define what happens when the user explicitly closes a window using the window manager.

You can use the protocol method to install a handler for this protocol (the widget must be a Tk or Toplevel widget):

Here you have a concrete example:

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

回答 1

马特展示了关闭按钮的一种经典修改。
另一种是使关闭按钮最小化窗口。
您可以通过将iconify方法
作为协议方法的第二个参数来重现此行为。

这是一个在Windows 7和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()

在此示例中,我们为用户提供了两个新的退出选项:
经典的File→Exit,以及Esc按钮。

Matt has shown one classic modification of the close button.
The other is to have the close button minimize the window.
You can reproduced this behavior by having the iconify method
be the protocol method’s second argument.

Here’s a working example, tested on Windows 7 & 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()

In this example we give the user two new exit options:
the classic File → Exit, and also the Esc button.


回答 2

取决于Tkinter活动,尤其是在使用Tkinter.after时,使用destroy()-即使通过使用protocol(),按钮等停止该活动也会干扰该活动(“执行时出错”),而不仅仅是终止它。在几乎每种情况下,最好的解决方案是使用标志。这是一个简单,愚蠢的用法示例(尽管我敢肯定,你们大多数人都不需要它!

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

这样可以很好地终止图形活动。您只需要running在正确的位置检查即可。

Depending on the Tkinter activity, and especially when using Tkinter.after, stopping this activity with destroy() — even by using protocol(), a button, etc. — will disturb this activity (“while executing” error) rather than just terminate it. The best solution in almost every case is to use a flag. Here is a simple, silly example of how to use it (although I am certain that most of you don’t need it! :)

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

This terminates graphics activity nicely. You only need to check running at the right place(s).


回答 3

我要感谢Apostolos的回答,这一点引起了我的注意。这是2019年Python 3的更加详细的示例,带有更清晰的描述和示例代码。


注意以下事实destroy():(或完全没有自定义窗口关闭处理程序)将在用户关闭窗口时立即销毁窗口及其所有正在运行的回调

这可能对您不利,具体取决于您当前的Tkinter活动,尤其是在使用tkinter.after(定期回调)时。您可能正在使用处理一些数据并将其写入磁盘的回调…在这种情况下,您显然希望数据写入完成而不会被突然终止。

最好的解决方案是使用标志。因此,当用户请求关闭窗口时,可以将其标记为一个标志,然后对其进行响应。

(注意:我通常将GUI设计为封装良好的类和单独的工作线程,并且我绝对不使用“全局”(我改用类实例变量),但这只是一个简单的示例,用于演示当用户关闭窗口时,Tk如何突然终止您的定期回调…)

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

此代码将向您显示WM_DELETE_WINDOW即使在periodic_call()工作/循环中间我们的自定义繁忙时,处理程序也会运行!

我们使用一些相当夸张的.after()值:500毫秒。这只是为了使它很容易让你看到关闭的区别,而周期性呼叫占线,或不…如果关闭,而数字更新,你会看到WM_DELETE_WINDOW发生了,而你的定期电话“是忙处理:是”。如果您在数字暂停时关闭(这意味着此时不处理定期回调),则会看到关闭发生在“不忙”期间。

在实际使用中,您.after()将使用30到100毫秒左右的时间来获得响应式GUI。这只是一个演示,可以帮助您了解如何保护自己免受Tk的默认“关闭时立即中断所有工作”的行为。

总结:让WM_DELETE_WINDOW处理程序设置一个标志,然后.destroy()在安全的情况下(当您的应用程序完成所有工作时)定期并手动检查该标志。

PS:您还可以使用WM_DELETE_WINDOW询问用户是否真的要关闭的窗口; 如果他们回答“否”,则无需设置标志。非常简单 您只需要在自己的信箱中显示一个消息框,WM_DELETE_WINDOW然后根据用户的答案设置标志即可。

I’d like to thank the answer by Apostolos for bringing this to my attention. Here’s a much more detailed example for Python 3 in the year 2019, with a clearer description and example code.


Beware of the fact that destroy() (or not having a custom window closing handler at all) will destroy the window and all of its running callbacks instantly when the user closes it.

This can be bad for you, depending on your current Tkinter activity, and especially when using tkinter.after (periodic callbacks). You might be using a callback which processes some data and writes to disk… in that case, you obviously want the data writing to finish without being abruptly killed.

The best solution for that is to use a flag. So when the user requests window closing, you mark that as a flag, and then react to it.

(Note: I normally design GUIs as nicely encapsulated classes and separate worker threads, and I definitely don’t use “global” (I use class instance variables instead), but this is meant to be a simple, stripped-down example to demonstrate how Tk abruptly kills your periodic callbacks when the user closes the window…)

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

This code will show you that the WM_DELETE_WINDOW handler runs even while our custom periodic_call() is busy in the middle of work/loops!

We use some pretty exaggerated .after() values: 500 milliseconds. This is just meant to make it very easy for you to see the difference between closing while the periodic call is busy, or not… If you close while the numbers are updating, you will see that the WM_DELETE_WINDOW happened while your periodic call “was busy processing: True”. If you close while the numbers are paused (meaning that the periodic callback isn’t processing at that moment), you see that the close happened while it’s “not busy”.

In real-world usage, your .after() would use something like 30-100 milliseconds, to have a responsive GUI. This is just a demonstration to help you understand how to protect yourself against Tk’s default “instantly interrupt all work when closing” behavior.

In summary: Make the WM_DELETE_WINDOW handler set a flag, and then check that flag periodically and manually .destroy() the window when it’s safe (when your app is done with all work).

PS: You can also use WM_DELETE_WINDOW to ask the user if they REALLY want to close the window; and if they answer no, you don’t set the flag. It’s very simple. You just show a messagebox in your WM_DELETE_WINDOW and set the flag based on the user’s answer.


回答 4

尝试简单版本:

import tkinter

window = Tk()

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

window.mainloop()

或者,如果您想添加更多命令:

import tkinter

window = Tk()


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


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

window.mainloop()

Try The Simple Version:

import tkinter

window = Tk()

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

window.mainloop()

Or If You Want To Add More Commands:

import tkinter

window = Tk()


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


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

window.mainloop()

回答 5

from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()
from tkinter import*
root=Tk()
exit_button=Button(root,text="X",command=root.quit)
root.mainloop()