Как обработать событие закрытия окна в Tkinter?

Как мне обрабатывать событие закрытия окна (нажатие кнопки "X" ) в программе Python Tkinter?

Ответ 1

Tkinter поддерживает механизм, называемый обработчики протоколов. Здесь термин "протокол" относится к взаимодействию между приложением и диспетчером окон. Наиболее часто используемый протокол называется WM_DELETE_WINDOW и используется для определения того, что происходит, когда пользователь явно закрывает окно с помощью диспетчера окон.

Вы можете использовать метод protocol для установки обработчика для этого протокола (виджет должен быть виджем Tk или Toplevel):

Здесь у вас есть конкретный пример:

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

Мэтт показал одну классическую модификацию кнопки закрытия.
Другой заключается в том, чтобы закрыть кнопку закрытия окна.
Вы можете воспроизвести это поведение, используя метод iconify protocol второй аргумент.

Вот рабочий пример, протестированный в Windows 7:

# Python 3
import tkinter
import tkinter.scrolledtext as scrolledtext

class GUI(object):

    def __init__(self):
        root = self.root = tkinter.Tk()
        root.title('Test')

    # 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')

gui = GUI()
gui.root.mainloop()

В этом примере мы даем пользователю два новых варианта выхода:
классическое меню файлов → Выход, а также кнопку Esc.

Ответ 3

В зависимости от активности Tkinter, и особенно при использовании Tkinter.after, остановка этой активности с помощью destroy() - даже с использованием protocol(), кнопки и т.д. - будет нарушать эту активность (ошибка "во время выполнения"), а не просто прекратить это. Лучшее решение почти в каждом случае - использовать флаг. Вот простой, глупый пример того, как его использовать (хотя я уверен, что большинству из вас это не нужно! :)

from Tkinter import *

def close_window():
  global running
  running = False
  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 в нужном месте (ах).

Ответ 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()

Ответ 5

Я хотел бы поблагодарить ответ апостола за то, что он привлек мое внимание. Вот гораздо более подробный пример для Python 3 в 2019 году, с более ясным описанием и примером кода.


Остерегайтесь того факта, что 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 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 произошло, когда ваш периодический вызов "был занят обработкой: True". Если вы закроете, когда номера приостановлены (это означает, что периодический обратный вызов в этот момент не обрабатывается), вы увидите, что закрытие произошло, когда оно "не занято".

В реальных условиях ваш .after() будет использовать что-то вроде 30-100 миллисекунд, чтобы иметь отзывчивый графический интерфейс. Это просто демонстрация, чтобы помочь вам понять, как защитить себя от Tk по умолчанию, "мгновенно прерывать всю работу при закрытии".

В итоге: сделайте так, чтобы обработчик WM_DELETE_WINDOW установил флаг, а затем периодически проверяйте этот флаг и вручную .destroy() окно, когда оно безопасно (когда ваше приложение выполнено со всей работой).

PS: Вы также можете использовать WM_DELETE_WINDOW, чтобы спросить пользователя, ДЕЙСТВИТЕЛЬНО ли он хочет закрыть окно; и если они отвечают нет, вы не устанавливаете флаг. Это очень просто. Вы просто показываете окно сообщения в своем WM_DELETE_WINDOW и устанавливаете флаг, основываясь на ответе пользователя.

Ответ 6

Использовать closeEvent

def closeEvent(self, event):
# code to be executed