0

I'm trying to make a tic tac toe game with tkinter. When I instantiate buttons in a 3x3 board that each call a function of mark_square() with themselves as a parameter, each button will refer to the last button that was instantiated in the nested for loop.

People have suggested that I bind the lambda function to the button which is what I did in this code:

from tkinter import *
from tkinter import messagebox

turn = "X"


def mark_square(box):
    global turn
    if box.cget('text') == " ":
        box['text'] = turn
    else:
        messagebox.showerror(title="Invalid", message="Invalid")


root = Tk()
root.title("Tic-Tac-Toe")

for x in range(0, 3):
    for y in range(0, 3):
        grid_box = Button(text=" ", font=("Arial", 40), padx=20, command=lambda grid_box=grid_box: mark_square(grid_box))
        grid_box.grid(row=x, column=y)

root.mainloop()

However, I'm getting an error telling me that grid_box is undefined. Does anyone know what's going on and how to fix it?

James Huang
  • 848
  • 1
  • 7
  • 35
  • 3
    You're not doing it correctly because you're trying to pass the value of `grid_box` as a the default value of an argument to the `lambda` function, however during the first iteration of the `for` loop, the variable doesn't exist yet which will cause an error…and even if it did not, each time it would be referring to the value of the last assignment to the variable (which would be for the **previous** `Button`). – martineau Jul 13 '20 at 02:05
  • 3
    `grid_box` has not been created yet. So use `grid_box.config(command=lambda ...)` after it is created. – acw1668 Jul 13 '20 at 02:09
  • Post the full trace back message. It shows the exact error and line that failed. – tdelaney Jul 13 '20 at 02:25

2 Answers2

1

As mentioned, in lambda grid_box=grid_box: that second grid_box is searching for a variable named "grid_box" in the current context, but its before you've assigned the Button to grid_box, so you get the error. Even if you defined grid_box it wouldn't work because the Button command callback takes no parameters.

A solution is to write your own subclass of Button that intercepts the command callback to add the button information you want. Conveniently, its the self of the button being initialized. When you assign an instance method to a callback, it knows its own "self" and that can be used to stash all manner of state useful for callbacks.

from tkinter import *
from tkinter import messagebox

turn = "X"


def mark_square(box):
    global turn
    if box.cget('text') == " ":
        box['text'] = turn
    else:
        messagebox.showerror(title="Invalid", message="Invalid")

class ButtonWithContext(Button):
    """Specializes tkinter.Button to allow a `command` that takes
    the button as a parameter"""

    def __init__(self, *args, **kwargs):
        try:
            self._my_command = kwargs["command"]
            kwargs["command"] = self.run_command
        except KeyError:
            self._my_command = None
        super().__init__(*args, **kwargs)

    def run_command(self):
        if self._my_command is not None:
            return self._my_command(self)


root = Tk()
root.title("Tic-Tac-Toe")

for x in range(0, 3):
    for y in range(0, 3):
        grid_box = ButtonWithContext(text=" ", font=("Arial", 40), padx=20,
            command=mark_square)
        grid_box.grid(row=x, column=y)

root.mainloop()
tdelaney
  • 73,364
  • 6
  • 83
  • 116
-1

Lambda functions can be used differently.

Try using below code:

from tkinter import *
from tkinter import messagebox

turn = "X"


def mark_square(box):
    global turn
    if box.cget('text') == " ":
        box['text'] = turn
    else:
        messagebox.showerror(title="Invalid", message="Invalid")


root = Tk()
root.title("Tic-Tac-Toe")

for x in range(0, 3):
    for y in range(0, 3):
        grid_box = Button(text=" ", font=("Arial", 40), padx=20, command=lambda grid_box: mark_square(grid_box))
        grid_box.grid(row=x, column=y)

root.mainloop()
Astik Gabani
  • 599
  • 1
  • 4
  • 11