2025, Oct 31 02:48
Модальный диалог в Tkinter: grab_set и wait_window с примерами
Разбираем, почему код в Tkinter не ждёт ввода из подокна, и как сделать диалог модальным: применяем grab_set и wait_window, блокируем окно и ждём закрытия.
Когда приложение Tkinter открывает подокно для ввода данных, остальная часть функции может продолжить выполняться сразу же. Итог знаком: всплывающее окно появилось, но основной код уже отработал так, будто пользователь дал ответ. Решение — сделать подокно модальным и явно дождаться его закрытия перед продолжением.
Постановка задачи
Главное окно запускает подокно, где пользователь выбирает вариант и нажимает Done. Однако основная функция продолжает выполнение сразу после создания подокна, поэтому читает выбор раньше, чем пользователь его сделает.
import tkinter as tk
class AppShell:
def __init__(self, master):
self.master = master
self.run_btn = tk.Button(master, text='Run', command=lambda: self.execute_flow(master))
self.run_btn.grid(row=1, column=0)
def execute_flow(self, parent):
stage = 2
if stage == 2:
ChoiceDialog(parent) # подокно создаётся, но код не ждёт
picked = ChoiceDialog.value # значение читается слишком рано
print(f'selected: {picked}')
root = tk.Tk()
AppShell(root)
root.mainloop()class ChoiceDialog:
value = ''
def __init__(self, parent):
self.win = tk.Toplevel(parent)
self.win.transient(parent)
self.win.title('User Input')
self.listbox = tk.Listbox(self.win, selectmode='browse', exportselection=tk.FALSE, relief='groove')
self.listbox.grid(row=0, column=0)
self.items = ['Selection1', 'Selection2', 'Selection3']
for opt in self.items:
self.listbox.insert(tk.END, opt)
tk.Button(self.win, text='Done', command=lambda: self.on_done(self.listbox, self.win)).grid(row=1, column=0)
def on_done(self, lb, toplevel):
sel = lb.curselection()
if len(sel) > 0:
val = lb.get(sel)
if val == 'Selection1':
ChoiceDialog.value = 'Selection1'
elif val == 'Selection2':
ChoiceDialog.value = 'Selection2'
elif val == 'Selection3':
ChoiceDialog.value = 'Selection3'
toplevel.destroy()
else:
ChoiceDialog.value = 'None'
toplevel.destroy()
print('No selection made')Что на самом деле происходит и почему
Подокно создаётся, но в основном потоке нет ничего, что велит Tkinter приостановиться, пока оно не будет закрыто. Tkinter сразу переходит к следующим строкам, то есть код пытается прочитать выбор до того, как пользователь нажмёт Done. В результате всплывающее окно видно уже после того, как остальная часть функции успела выполниться.
Решение: сделать подокно модальным и подождать
Это решается двумя вызовами. Во‑первых, заблокируйте взаимодействие с главным окном, чтобы пользователь не открывал несколько подокон: вызовите grab_set у подокна. Во‑вторых, явно дождитесь его закрытия: вызовите wait_window с этим окном. Оба шага удобно выполнить в инициализаторе диалога, чтобы место вызова оставалось чистым.
import tkinter as tk
class AppShellFixed:
def __init__(self, master):
self.master = master
self.run_btn = tk.Button(master, text='Run', command=self.execute_flow)
self.run_btn.grid(row=1, column=0)
def execute_flow(self):
stage = 2
if stage == 2:
dlg = ChoiceDialogModal(self.master) # создание блокирует выполнение до закрытия
picked = dlg.selection # к этому моменту точно установлено
print(f'selected: {picked}')
root = tk.Tk()
AppShellFixed(root)
root.mainloop()class ChoiceDialogModal:
def __init__(self, parent):
self.top = tk.Toplevel(parent)
self.selection = None
self.lb = tk.Listbox(self.top)
self.lb.grid(row=0, column=0)
for opt in ['Selection1', 'Selection2', 'Selection3']:
self.lb.insert(tk.END, opt)
tk.Button(self.top, text='Done', command=self.handle_done).grid(row=1, column=0)
self.top.grab_set() # блокируем доступ к главному окну
self.top.master.wait_window(self.top) # ждём, пока подокно не будет закрыто
def handle_done(self):
picked = self.lb.curselection()
if len(picked) > 0:
self.selection = self.lb.get(picked)
else:
self.selection = None
print('No selection made')
self.top.destroy()Подробности, улучшающие логику
Ожидание подокна гарантирует, что основная функция продолжит работу только после того, как пользователь завершит ввод. grab_set не даёт взаимодействовать с родительским окном в это время и тем самым предотвращает повторный запуск того же диалога. Хранение результата в экземпляре диалога делает состояние локальным и удобным для доступа, а вызовы методов и обращение к виджетам через self избавляют от передачи виджетов по параметрам. Использование CamelCaseNames для классов, например ChoiceDialogModal, повышает читаемость и согласует стиль имен с классами Tkinter, такими как Frame и Button. Если подойдёт готовое решение, можно присмотреться к диалогам Tkinter из стандартной документации.
Почему это важно
В GUI‑коде порядок выполнения решает всё. Без явного ожидания логика, зависящая от пользовательского ввода, запускается в неполном состоянии — это ведёт к неверным веткам, пустым значениям или повторным запросам. Модальное подокно и ожидание его закрытия дают детерминированный поток выполнения и более чистый пользовательский опыт.
Вывод
Чтобы приостановить выполнение, пока подокно Tkinter не вернёт выбор пользователя, создайте Toplevel, вызовите grab_set для блокировки главного окна и wait_window, чтобы приостановить вызывающий код до уничтожения подокна. Храните выбранное значение в атрибуте экземпляра и обращайтесь к виджетам через self — так яснее. Этот приём гарантирует, что оставшаяся часть функции запустится только после того, как пользователь сделает выбор — именно тогда, когда это требуется.