Kurt Peek Kurt Peek - 6 months ago 73
Python Question

How to make an array of buttons which change text upon clicking using Tkinter

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

class ChangeButton:

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]))
self.buttons[i][j].grid(row=i, column=j)

def toggle_text(self, button):
if button["text"] == "":
button["text"] = "X"

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

app = ChangeButton(root)


However, the resulting window looks like this:

enter image description here

and the buttons don't change when clicked. Any ideas why this does not work?


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. i and 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, i and j are evaluated when the function is declared, not when it is called.

Your 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)]