2025, Dec 11 18:02

Как передать координаты ячейки tk.Button в Tkinter через lambda

Почему все tk.Button вызывают один колбэк в Tkinter и как это исправить: передаём координаты ячейки через lambda с аргументами по умолчанию — для Сапёра.

Когда вы создаёте сетку виджетов tk.Button во вложенных циклах, все они могут вызывать один и тот же колбэк, не передавая никаких подсказок о том, какая ячейка была нажата. В «Сапёре» это делает невозможным понять, щёлкнул ли пользователь по (2, 3) или по (7, 8). Решение довольно простое, если понимать, как Python привязывает переменные в колбэках.

Постановка задачи

Ниже — минимальный фрагмент, который строит поле 10×10 на Canvas и назначает один общий обработчик всем кнопкам. В нём суть проблемы: колбэк не получает координат, поэтому определить, по какой ячейке кликнули, нельзя.

import tkinter as tk
# упрощённый обработчик, общий для всех кнопок
def on_cell():
    pass
pos_x = -400
pos_y = -200
cell_idx = 0
btn_cells = []
# представьте, что этот canvas получен из turtle через screen.getcanvas()
root = tk.Tk()
cnv = tk.Canvas(root)
cnv.pack()
for r in range(10):
    for c in range(10):
        btn_cells.append([tk.Button(cnv.master, text="////", command=on_cell)])
        cnv.create_window(pos_x, pos_y, window=btn_cells[cell_idx])
        cell_idx += 1
        pos_x += 40
    pos_y += 40
    pos_x += -400
root.mainloop()

Каждый виджет указывает на одну и ту же функцию без аргументов, поэтому в момент клика у вас нет ни строки, ни столбца. Полагаться на координаты мыши ненадёжно для сетки, привязанной к логическому полю.

Что именно идёт не так

Использовать одну функцию — нормально, но ей нужны стабильные, «по-кнопочные» данные о позиции. Если замкнуть в колбэке прямо переменные цикла, Python подставит их значения в момент вызова, а не при создании. После завершения циклов эти переменные обычно хранят последние значения, и все кнопки «сообщают» одни и те же координаты. Надёжный способ — привязать текущую строку и столбец при определении колбэка через аргументы по умолчанию.

Решение: зафиксировать индексы через аргументы по умолчанию

Передайте в command лямбду, которая «замораживает» текущие индексы цикла как значения по умолчанию и передаёт их в функцию, ожидающую строку и столбец. Код остаётся компактным, а каждая кнопка будет сообщать свои координаты.

#!/usr/bin/python3
import tkinter as tk
def cell_hit(rx, cx):
    print("Hello from ({}, {})!".format(rx, cx))
app = tk.Tk()
for r in range(10):
    for c in range(10):
        tk.Button(text="{}, {}".format(r, c),
                  command=lambda rr=r, cc=c: cell_hit(rr, cc))\
                  .grid(column=r, row=c)
app.mainloop()

Ключевой момент — command=lambda rr=r, cc=c: cell_hit(rr, cc). Значения rr и cc по умолчанию фиксируют текущие r и c как константы для конкретной кнопки. Когда пользователь кликает, сохранённые значения передаются в cell_hit, и вы сразу знаете, какая ячейка сработала.

Та же идея для раскладки на Canvas

Если размещаете кнопки внутри Canvas, можно оставить логику раскладки как есть и лишь изменить command, чтобы захватывать индексы. Подход тот же: привязывайте данные каждой кнопки при создании.

import tkinter as tk
def cell_open(rx, cx):
    print("Clicked cell ({}, {})".format(rx, cx))
x0 = -400
y0 = -200
k = 0
widgets = []
root = tk.Tk()
cv = tk.Canvas(root)
cv.pack()
for r in range(10):
    for c in range(10):
        btn = tk.Button(cv.master, text="////", command=lambda rr=r, cc=c: cell_open(rr, cc))
        widgets.append([btn])
        cv.create_window(x0, y0, window=widgets[k])
        k += 1
        x0 += 40
    y0 += 40
    x0 += -400
root.mainloop()

Это сохраняет исходную структуру и даёт каждой Button устойчивую ссылку на собственные координаты строки и столбца.

Почему это важно

Колбэки в GUI часто создаются внутри циклов — не только в простых играх. Понимание того, что Python подставляет свободные переменные при вызове функции, а аргументы по умолчанию фиксируют значения при определении, помогает избежать коварных багов, когда все виджеты ведут себя так, будто они последний созданный. Та же техника сохраняет код опрятным: не нужно протаскивать координаты через глобальное состояние или вычислять позиции по событиям указателя.

Если вам удобнее другой синтаксис, того же эффекта можно добиться с помощью functools.partial — он тоже заранее привязывает аргументы. Суть остаётся той же: создать для каждой кнопки замыкание с нужными индексами.

Итоги

Делайте колбэки самодостаточными. Генерируя кнопки в цикле, фиксируйте переменные цикла прямо в command. Аргументы по умолчанию в лямбде — краткий и надёжный способ, который хорошо масштабируется по мере роста поля или усложнения логики. С этой небольшой правкой каждый клик несёт свой контекст, и остальная логика сапёра может сосредоточиться на геймплее, а не на угадывании, какую ячейку нажали.