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.