I'm trying to implement a simple GUI for the game of Tic Tac Toe using Tkinter. As a first step, I'm trying to make an array of buttons which change from being unlabeled to having the "X" label when clicked. I've tried the following:
import Tkinter as tk
def __init__(self, master, grid=np.diag(np.ones(3))):
frame = tk.Frame(master)
self.grid = grid
self.buttons = [[tk.Button()]*3]*3
for i in range(3):
for j in range(3):
self.buttons[i][j] = tk.Button(frame, text="", command=self.toggle_text(self.buttons[i][j]))
def toggle_text(self, button):
if button["text"] == "":
button["text"] = "X"
root = tk.Tk()
root.title("Tic Tac Toe")
app = ChangeButton(root)
The primary problem is that with
command=self.toggle_text(self.buttons[i][j])), you invoke the callback function and bind its result to
command. Instead, you have to bind the function itself tocommand
, or alambda` that will invoke that function with the right parameters. A naive way of doing this would look like this:
command=lambda: self.toggle_text(self.buttons[i][j]) # won't work!
But this will not work inside the loop, as the variables inside the
lambda are evaluated when the function is executed, i.e. after the loop, i.e.
j will take on the last value for each of the functions. For a more detailed explanation, see e.g. here. One way to fix this is to declare those variables as parameters to the
lambda, and at the same time use the current values from the loop as their default values, i.e.
lambda i=i, j=j: .... This way,
j are evaluated when the function is declared, not when it is called.
command and the surrounding loop would then look like this:
for i in range(3): for j in range(3): self.buttons[i][j] = tk.Button(frame, text="", command=lambda i=i, j=j: self.toggle_text(self.buttons[i][j])) self.buttons[i][j].grid(row=i, column=j)
And there is a another problem, unrelated to the first, with the way you initialize the
self.buttons list. By doing
[[tk.Button()]*3]*3, the list will hold three references to the same list, each holding three references to the same button. See e.g. here for a more in-depth discussion. Also, you do not need to initialize the buttons in the list at all, as you set those afterwards, in the loop. I'd suggest using a nested list-comprehension instead:
self.buttons = [[None for _ in range(3)] for _ in range(3)]