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 — так яснее. Этот приём гарантирует, что оставшаяся часть функции запустится только после того, как пользователь сделает выбор — именно тогда, когда это требуется.

Статья основана на вопросе на StackOverflow от Juliette Mitrovich и ответе от furas.