2025, Oct 16 16:16
Связываем зависимые ComboBox в Tkinter: чистый и надежный подход
Как связать зависимые ComboBox в Tkinter: словарь соответствий, обработчик <<ComboboxSelected>>, обновление .config(values) и StringVar для корректного UI.
Создать небольшой инструмент на Tkinter для записи обслуживания станков несложно, пока не появляются зависимые поля: один ComboBox управляет вариантами другого. Частая ловушка — изменить лишь переменную в Python и ожидать, что интерфейс это отразит. Не отразит. Ниже — краткое объяснение проблемы и чистый, надёжный способ связать зависимые виджеты ComboBox в Tkinter.
Проблема
Задача — показывать список работ во втором ComboBox в зависимости от выбранного станка в первом. Обработчик вычисляет, какой список использовать, но интерфейс не обновляется, и второй выпадающий список продолжает отображать исходную заглушку.
import tkinter as tk
from tkinter import ttk
tasks_pool = ['Maybe it will work this time?']
def machine_task_select(event):
    """generate task combobox based on machine selection"""
    global tasks_pool
    jf_tasks = ['Coolant', 'Waylube', 'Spindle Oil', 'Laser Reflector', 'Air Filter', 'Check Unlcamp/Clamp', 'Check ATC']
    dia_tasks = ['Coolant', 'Spindle Cone', 'Clean Blume Laser', 'Check Spindle Oil', 'Check Grease', 'Check Fluid Press', 'Check Chiller', 'Clean Chip Basket Recovery Tray', 'Electrical Cabinet AC Air Filter', 'Column Air Filter']
    tak_tasks = ['Coolant', 'Spindle Oil', 'Waylube', 'Air Filter']
    act3_tasks = ['Coolant', 'Spindle Cone', 'Clean Blume Laser', 'Spindle Oil','Waylube', 'Coupling Lube', 'Air Filter']
    hart_tasks = ['Coolant', 'Oil', 'Air Filter']
    herm_tasks = ['Coolant', 'Grease', 'Air Filter']
    choice = str(box_machine.get())
    if choice == machines[0] or choice == machines[1] or choice == machines[2] or choice == machines[3]:
        tasks_pool = jf_tasks
    elif choice == machines[4] or str(box_machine.get()) == machines[5]:
        tasks_pool = dia_tasks
    elif choice == machines[6]:
        tasks_pool = tak_tasks
    elif choice == machines[7]:
        tasks_pool = act3_tasks
    elif choice == machines[8]:
        tasks_pool = hart_tasks
    elif choice == machines[9]:
        tasks_pool = herm_tasks
    else:
        tasks_pool = ['Am I Insane?']
    return tasks_pool
app = tk.Tk()
app.title("Maintenance Recorder")
app.resizable(width=False, height=False)
panel = tk.Frame(master=app)
panel.pack()
machines = ['JF1', 'JF2', 'JF1500', 'JF1600', 'Diamond 1', 'Diamond 2', 'Takumi B8', 'Active 3000', 'Hartford', 'Hermle C400U']
box_machine = ttk.Combobox(master=panel, width=50, values=machines, state='readonly')
box_machine.bind('<<ComboboxSelected>>', machine_task_select)
lbl_machine = tk.Label(master=panel, text='Machine:')
box_task = ttk.Combobox(master=panel, width=50, values=tasks_pool, state='readonly')
lbl_task = tk.Label(master=panel, text='Task Completed:')
lbl_machine.grid(row=0, column=0, sticky='e')
box_machine.grid(row=0, column=1, sticky='w')
lbl_task.grid(row=4, column=0, sticky='e')
box_task.grid(row=4, column=1, sticky='w')
app.mainloop()
Что на самом деле происходит
Обработчик вычисляет, какой список задач нужно показать, но комбобоксу так и не сообщают, что надо взять новый список. Изменение переменной в Python не меняет состояние виджета. Виджеты Tkinter не привязаны к произвольным глобальным переменным; чтобы поменять содержимое ttk.Combobox, нужно явно обновить его значения через конфигурацию виджета и, если используется textvariable, задать его тоже.
Есть и вопрос сопровождаемости: длинную цепочку if/elif можно заменить прямым отображением станка на список задач. Тогда обновление UI становится тривиальным и менее подверженным ошибкам.
Решение
Используйте словарь, который сопоставляет выбор в первом ComboBox со списком вариантов для второго. Привяжите к первому ComboBox обработчик, обновляющий значения второго и выбирающий значение по умолчанию. Так поведение остаётся предсказуемым, а код — компактным.
import tkinter as tk
from tkinter import ttk
def set_secondary_options(_evt) -> None:
    """Update the values in secondary_box when primary_box changes"""
    picked = sel_primary.get()
    options = mapping[picked]
    secondary_box.config(values=options)
    sel_secondary.set(options[0])
ui = tk.Tk()
# ключи заполняют первый combobox; список у каждого ключа заполняет второй
mapping = {
    'foo': ['fee', 'fi', 'fo', 'fum'],
    'bar': ['fizz', 'buzz'],
    'baz': ['zip', 'zap', 'zoop'],
}
first_keys = list(mapping.keys())
second_defaults = mapping[first_keys[0]]
# переменные, хранящие текущие выбранные значения
sel_primary = tk.StringVar(ui, value=first_keys[0])
sel_secondary = tk.StringVar(ui, value=second_defaults[0])
# первый комбобокс с обработчиком для обновления второго
primary_box = ttk.Combobox(ui, values=first_keys, textvariable=sel_primary)
primary_box.bind('<<ComboboxSelected>>', set_secondary_options)
# второй комбобокс
secondary_box = ttk.Combobox(ui, values=second_defaults, textvariable=sel_secondary)
primary_box.pack(side='left', padx=5, pady=5)
secondary_box.pack(side='left', padx=5, pady=5)
ui.mainloop()
Почему это важно
Зависимые поля встречаются повсюду во внутренних инструментах и утилитах для ввода данных. Если интерфейс обновляется непредсказуемо, пользователи видят устаревшие варианты и получают несогласованные записи. Явная переконфигурация состояния виджета по событию — это разница между полем, которое выглядит корректно, и полем, которое действительно корректно.
В заключение
Связывая зависимые ComboBox в Tkinter, используйте структуру отображения для наглядности, привязывайтесь к событию '<<ComboboxSelected>>' у управляющего элемента и обновляйте зависимый виджет через .config(values=...). Если поддерживаете значение по умолчанию, сразу после обновления values установите StringVar в допустимый вариант. Для читабельности следуйте PEP 8: используйте lower_case_names для функций и переменных и оставляйте CamelCaseNames для классов, таких как Frame и Label. С этими небольшими практиками динамические выпадающие списки остаются простыми, предсказуемыми и легко расширяемыми.
Статья основана на вопросе с StackOverflow от TheGman117 и ответе JRiggles.