Tkinter pass a button as argument - python

I'm new to Tkinter and as my first project I wanted to create a Tic Tac Toe.
I want to create 9 buttons, that will change their background image when I click on them, the problem is that I dont want to create a function for every single button but one function that will take the button in argument and will change its background image.
The code I wrote:
def play(bid):
if player == "X":
bid.config(image=cross)
if player == "O":
bid.config(image=circle)
b1 = tk.Button(app, text="", image=white, command=lambda id=b1: play(id))
b1.grid(column=0, row=0)
How can I pass b1 as an argument to play() function?
Thanks
I tried to use b1 as an argument to play(), and use play() to change b1's image.
When I try to run this code I get "name b1 is not defined".

Define a single function to create, configure, and bind your button; then your callback can close over the necessary variable referring to your button. Something like
def play(bid):
if player == "X":
bid.config(image=cross)
if player == "O":
bid.config(image=circle)
def add_button(app, r, c):
b = tk.Button(app, text="", image=white)
b.config(command=lambda: play(b))
b.grid(column=c, row=r)
return b
for row in [0,1,2]:
for col in [0,1,2]:
# Save the return value somewhere if necessary
addButton(app, row, col)
The lambda expression contains a free variable b, which refers to the variable b in the closest enclosing scope, which is the call to add_button where the lambda expression is evaluated.

One way you can do it is to separate the creation of the button from the assignment using the .config method.
b1 = tk.Button(app, text="", image=white)
b1.config(command=lambda btn_id=b1: play(btn_id))
This is not really the best way to go about it, it's better to instead pass something that defines the button like an Enum or text.
b1 = tk.Button(app, text="", image=white, command=lambda : play("b1"))
then check that "b1" is the input in your function, this way b1 can be redefined as what would happen in loops.

Here is what I wrote at my leisure, it may be useful. Just create a folder 'icons' and put 100x100 px images there
from tkinter import *
import random
class Main:
def __init__(self):
self.root = Tk()
#self.root.geometry('900x100')
def run(self):
self.variables()
self.interface()
self.root.mainloop()
def variables(self):
self.PHOTO_COUNTER = 0
self.photo_list = [
PhotoImage(file="icons/bublegum.png"),
PhotoImage(file="icons/fin.png"),
PhotoImage(file="icons/jake.png"),
PhotoImage(file="icons/marcelin.png"),
PhotoImage(file="icons/navel.png"),
PhotoImage(file="icons/winter_king.png"),
]
def interface(self):
self.Buttons = []
for i in range(10):
item = random.choice(self.photo_list)
self.btn = Button(self.root, image=item, command=lambda c=i: self.click(c))
self.btn.pack(fill=BOTH, expand=1, side=LEFT)
self.Buttons.append(self.btn)
def click(self, i):
btn = self.Buttons[i]
item = random.choice(self.photo_list)
btn.config(image=item)
A = Main()
A.run()

Related

Tkinter: Changing image of Radiobutton using another Radiobutton works only the first time

I am trying to create a game level editor. The program reads a binary file, associate the byte to a image and then, with a loop, it shows all the images in canvas (Canvas B). The images are Radiobutton. This part works properly.
Below (Canvas A) I have a set of images that are radiobuttons too. Here I choose the image I want to use by clicking on it. This image will be used to redesign the Canvas B Then I go to the Canvas B, decide which tile I want to change and click on it. And it works: the radiobutton has now the chosen image.
In fact it works every time but only the first time. If I change my mind and want to change a radiobutton already changed nothing happens.
I tried to understand what the problem is by printing the variable of the radiobutton with .get()and I see that it stored the value of the last rabiobutton clicked. I tried to reset this value even with del but it doesn't work.
Here's the code (canvas B)
img_list=[]
n_row = 0
n_col = 0
index = 0
x = IntVar()
for f in os.listdir(path):
img_list.append(ImageTk.PhotoImage(Image.open(os.path.join(path,f))))
n_col +=1
index +=1
if n_col > 21:
n_row +=1
n_col = 1
tile = Radiobutton(A, image=img_list[index-1], indicatoron=0, bd=2, variable = x, value = index, selectcolor="red", command=several)
tile.grid(row=n_row, column = n_col)
And here's Canvas A
def erase():
global val_t_e_x
del val_t_e_x
val_t_e_x=t_e_x.get()
print(val_t_e_x)
img_qui=[]
for f in os.listdir(path):
img_qui.append(ImageTk.PhotoImage(Image.open(os.path.join(path,f))))
def several_editor():
global codice_bb
global val_x
global val_t_e_x
val_t_e_x=t_e_x.get()
print(val_t_e_x)
row_qui=row_list[val_t_e_x-1]
col_qui=col_list[val_t_e_x-1]
tile_editor=Radiobutton(B, image=img_qui[val_x-1], variable = val_t_e_x, value = rev, indicatoron=0, bd=0, selectcolor="blue",
highlightbackground="black", highlightcolor="black", command=erase)
tile_editor.grid(row=row_qui, column=col_qui)
col_b=0
row_b=9
l_editor=[]
row_list=[]
col_list=[]
rev=0
t_e_x = IntVar()
for x, y in zip(line[::2], line[1::2]):
a= ("./gfx/"+(x+y)+".png")
row_b-=1
rev+=1
if row_b<1:
col_b+=1
row_b=8
im = Image.open(a)
ph = ImageTk.PhotoImage(im)
l_editor.append(ph)
tile_editor = Radiobutton(B, image=l_editor[rev-1], variable = t_e_x, value = rev, indicatoron=0, bd=0, selectcolor="blue",
highlightbackground="black", highlightcolor="black", command=several_editor)
tile_editor.grid(row=row_b, column=col_b)
row_list.append(row_b)
col_list.append(col_b)
I suppose that the problem is in the function def several_editor()
tile_editor=Radiobutton(B, image=img_qui[val_x-1], variable = val_t_e_x, value = rev,
indicatoron=0, bd=0, selectcolor="blue", highlightbackground="black",
highlightcolor="black", command=erase)
and that I am not handling the val_t_e_x variable properly.
Hope you can help me.
The reason your function isn't returning is because you don't know what the input is for several_editor(). You need to anonymize it with lambda.
I'm going to provide a simple example of why this is necessary:
import tkinter, os, sys
import tkinter.ttk as ttk
class T():
def __init__(self):
labelstr=["ascii"]
top = tkinter.Tk()
top.geometry('200x50')
top.title('lambda')
for label in labelstr:
self.lab = tkinter.Label(top, text=label, width=len(label)).grid(row=labelstr.index(label)+1, column=0)
self.ent = tkinter.Entry(top, width=10).grid(row=labelstr.index(label)+1, column=1)
but = tkinter.Button(top, text="submit", command=lambda: self.submit(top))
but.grid(row=labelstr.index(label)+1, column=3)
top.mainloop()
def submit(self, frame):
for i in frame.winfo_children():
if i.winfo_class() == "Entry":
i.delete(0, tkinter.END)
i.insert(0, "HelloWorld!")
I can't really test the code where I am at the moment, but this should work.
The above is to provide a functional example of proper use of a lambda function within your code and demonstrate why you'd need it. Try removing the lambda and see that the function runs only once and will no longer run properly. This symptom should look familiar to you as it's exactly as you described it. Add the lambda back and the function runs again each time you hit the button.

How to return a selected itemvalue from listbox from one module to another?

i want to return the selected item from a listbox from one module into another. Inside A.py i call a function from B.py which creates a small tkinter listbox. Within this listbox i want to select an item, and return it as a value to A.py.
I tried several things, i.e.
# inside A.py
import B
filter = B.get_pt_filter()
and
# inside B.py
from tkinter import * # (i know this is not clean, but let's keep it this way for now)
from functools import partial
def button_click(lb):
return lb.get(lb.curselection())
def get_pt_filter():
pt_list = define_pt_list()
# create window
tk_window = Tk()
tk_window.title('Choose')
tk_window.geometry('350x400')
# listbox border
frame_listbox = Frame(master=tk_window, bg='#FFCFC9')
frame_listbox.place(x=5, y=5, width=335, height=360)
# button border
frame_button = Frame(master=tk_window, bg='#D5E88F')
frame_button.place(x=5, y=370, width=335, height=30)
# Listbox
lb = Listbox(master=frame_listbox, selectmode='browse')
for pt in pt_list:
lb.insert('end', pt)
lb.place(x=5, y=5, width=325, height=350)
# Label Text
labelText = Label(master=frame_button, bg='white')
labelText.place(x=5, y=5, width=325, height=20)
# button_choose = Button(master=frame_button, text='Choose', command= lambda: button_click(lb))
action_with_arg = partial(button_click, lb)
button_choose = Button(master=frame_button, text='Choose', command=action_with_arg)
button_choose .place(x=5, y=5, width=325, height=20)
# Aktivierung des Fensters
tk_window.wait_window()
def define_pt_list():
return [
'apple'
]
So far i get the value i want in button_click but i fail to return it to module A.py and assign it to filter. How would i do that? What am i missing here?
I've tried several things including a return button_choose at the end of get_pt_filter or lambda functions behind command while creating Button. I can not break the Loop however, and i'm stuck forever it seems like. Also i tried tk_window.mainloop() and tk_window.wait_window(). None of those work.
In Short: How do i assign "apple" to filter?
So the problem here is that although lb.get(lb.curselection()) does actually evaluate to "apple", button_click has nowhere to return that value. You can verify this by having the button_click function printing instead of returning. In order to use as much of the code you've already written i suggests that you create a global variable in button_click, e.g.:
def button_click(lb):
global selection
selection = lb.get(lb.curselection())
Then you can access the variable selection in get_pt_filter and thus return it from there with return selection.
To make sure that the 'choose'-button actually closes the window, you should make the 'root' global as well:
global tk_window
tk_window = Tk()
and finish button_click of with tk_window.quit(), so the function ends up looking like this:
def button_click(lb):
global selection
selection = lb.get(lb.curselection())
tk_window.quit()
Also replace tk_window.wait_window() with tk_window.mainloop().
This is a bit of a hacky solution, but it fits the code you already wrote.
A better solution would be to store the application in a class that has it's own variables that can be accessed by button_click. Have a look at this thread. I suggest the following application to accomplish what you need without using global:
from tkinter import *
class get_pt_filter:
def __init__(self, parent, pt_list):
# create window
self.tk_window = parent
self.tk_window.title('Choose')
self.tk_window.geometry('350x400')
# listbox border
frame_listbox = Frame(master=self.tk_window, bg='#FFCFC9')
frame_listbox.place(x=5, y=5, width=335, height=360)
# button border
frame_button = Frame(master=self.tk_window, bg='#D5E88F')
frame_button.place(x=5, y=370, width=335, height=30)
# Listbox
self.lb = Listbox(master=frame_listbox, selectmode='browse')
for pt in pt_list:
self.lb.insert('end', pt)
self.lb.place(x=5, y=5, width=325, height=350)
# Label Text
labelText = Label(master=frame_button, bg='white')
labelText.place(x=5, y=5, width=325, height=20)
button_choose = Button(master=frame_button, text='Choose', command=self.button_click)
button_choose.place(x=5, y=5, width=325, height=20)
def button_click(self):
self.selection = self.lb.get(self.lb.curselection())
self.tk_window.quit()
if __name__ == "__main__":
parent = Tk()
get = get_pt_filter(parent, ['apple', 'pear'])
# Aktivierung des Fensters
parent.mainloop()
#access the selection
print(get.selection)
When mainloop quits you can access the selection varaible of the instance.

How to use for loop to create buttons that can be then each configured separately in my command function?

I have created 9 buttons with a loop and want each of them to display "x" when clicked. However, the command function is not properly executing for each button.
I already tried using lambda... I think the issue might be in the way that I named each Button?
def create_buttons(self):
buttoncounter = 1
for i in range(9):
self.temp_string = "b" + str(buttoncounter)
self.temp_string = Button(self, text = "\n\t\n\t\n\t")
self.temp_string.grid(row = (20 + i), column = (20))
self.temp_string.configure(command=partial(self.mark_box,
buttoncounter))
buttoncounter += 1
def mark_box(self, num):
turnlist = ["x", "o"]
self.temp_string = "b" + str(num)
self.temp_string.configure(text = "x")
I want to be able to click a button and have it check itself off, but when I click any of the 9 buttons it only checks off the 9th one.
To access widgets created in a loop we use dictionaries and list to keep a reference to them. Later we can modify them from their references stored inside dictionaries or list.
Like so..
all_buttons = []
for i in range(9):
button = Button(root, .... )
all_buttons.append(button)
When we need to get a specific button we can get it by all_buttons[0] which will gives us the instance of the Button first created in the loop.
But, if you want to give your own tag or name to refer each Button then use dictionary where key will be the name and value will be the instance to the Button.
all_buttons = {}
for i in range(9):
button = Button(root, .... )
all_buttons.update({ 'Button_%s'%i : button })
To refer, we use all_buttons['Button_0'] gives us the first created Button.
Now, How to pass commands to each button in a loop.
I see you're using partial from functools to pass argument to the function mark_box, we can also use lambda to get the same results without importing functools. Refer to this post for better understanding.
Here is a combined example of how to pass arguments in a loop to the callback function of Button using lambda and also keep the reference to the Button?
import tkinter as tk
root = tk.Tk()
label = tk.Label(root, text='Click the Button')
label.pack()
def update(text):
label.config(text="Button %s is clicked"%text)
all_buttons = []
for i in range(9):
button = tk.Button(root, text='Button %s'%i, command=lambda i=i: update(i) )
button.pack()
all_buttons.append(button)
print('\nThis the list containing all the buttons:\n', all_buttons)
root.mainloop()

Passing an object in lambda in tkinter button callback

I am trying to use lambda to create callbacks for tkinter buttons.
There are multiple buttons and each callback needs to pass an object inside it. Following code is what I am doing and is running fine
var0 = tk.StringVar()
label = tk.Label(top, bg = "White",height = 2, width = 12,textvariable=var0, justify="right")
def b0Callback(var):
var.set(var.get()+"0")
return
# creating a label which will print value of the any of the 0-9 button pressed
# creating a button 0
b0 = tk.Button(numFrame0, height = 1, width = 4, bg = "grey", text =
"0",command = lambda: b0Callback(var0))
#there are more buttons like that
var0 is used to update a label. Above code is working fine but I have to create callback for 0 to 9 and I have to just repeat above definition. So I tried using following example from this tutorial
def myfunc(n):
return lambda a : a * n
mydoubler = myfunc(2)
mytripler = myfunc(3)
print(mydoubler(11))
print(mytripler(11))
Using it I did following
def Callback(n):
return lambda var.set(var.get()+n)
b0Callback = Callback("0")
This shows error invalid index in the return line at var.set
Is there any way to pass var0 in this case to avoid this error?
Maybe its only me, but I don't see a reason for using lambda if you just want to add a number to the label text.
Lets make a function for it that gets your StringVar() as a variable and adds some number to it:
def button_callback(str_var, number):
str_var.set(str_var.get() + str(number))
To run this command we simply put it in the code as a lambda function, otherwise it will run upon initialization (because we are providing a function instead of a reference).
So to run it within a button we declare it like this:
my_button = Button(root, text='Some text here', command=lambda: button_callback(my_string_var, 5))
The '5' could be potentially changed to any other number.
I have now solved the problem, here is the final code:
I have also changed the number of buttons to 300 and added code to arrange them all in a nice grid, just for fun. (You can change this to however many you want by changing for number in range(1, whatever).
import tkinter as tk
class Window(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.var0 = tk.StringVar()
self.var0.set('0')
# creating buttons and adding them to dictionary
self.buttons = {}
for number in range(1, 301):
self.buttons.update({'button' + str(number): tk.Button(self, height=1, width=4, bg="grey", text=number)})
label = tk.Label(self, textvariable=self.var0, font='none 50')
label.grid(column=0, row=0)
for button in self.buttons:
self.buttons[button].bind('<Button-1>', lambda event, num=button[6:]: self.Callback(event, num))
self.buttons[button].grid(column=(int(button[6:]) % 10), row=(int(button[6:]) / 10) + 1)
def Callback(self, event, num):
self.var0.set(num)
self.update()

Producing multiple buttons using a for loop and being able to differentiate between them and run separate code on mouse click.

So I am trying to create a function that automatically creates 10 buttons using a for loop and positions them on screen and name them 1 to 10. Then when pressed a parameter that was passed during their creation instructs another function which button was pressed and adds different items to a list.
def button_placement():
mover = 227
button_number = 1
for items in range(10):
button_number = IntVar()
Button(canvas1, width="5", height="2", textvariable=button_number,
command= lambda: button_action(button_number)).place(x=150, y=mover)
mover = mover + 50
button_number = button_number +1
def button_action(button_identifier):
global list2
global list1
for buttons in range(1,10):
if button_identifier == buttons:
if len(list1) > 1:
list2.append(list1[buttons])
Where both list 1 and 2 have items in them already. And please excuse the global variables.At the moment something is wrong with the IntVar operand in button_placement. Thanks in Advance!
You can't use lambda in this situation because it's late binding. Use functools.partial instead. Also, you don't need IntVar, use a normal python integer.
from functools import partial
def button_placement():
mover = 227
for button_number in range(10):
btn = Button(canvas1, width="5", height="2", text=button_number,
command= partial(button_action, button_number))
btn.place(x=150, y=mover)
mover = mover + 50
I'm not sure why you're putting the Buttons on a Canvas, nor exactly what you're trying to do with the two global lists—so I left them out and just made the command function print() what button identifier it's now being passed as an argument.
That said, here's a runnable example that shows how to pass extra arguments to the Button's command handler function by defining it to have a default argument with the desired value in it when declaring the lambda function.
While this can also be done using partial as described in #Novel's answer, but this approach is probably the more common way it's done—and is more concise than using functools. Either way, you don't need to use an IntVar to accomplish what you want since you'll effectively be passing the value to the function now.
from tkinter import *
def button_placement(canvas):
xpos, ypos = 150, 25
for button_number in range(1, 11):
button = Button(canvas, width='5', height='2', text=button_number,
anchor=CENTER, command=
lambda id=button_number: button_action(id))
window = canvas.create_window(xpos, ypos, window=button)
ypos += 50
def button_action(button_identifier):
print('Button {} pressed'.format(button_identifier))
root = Tk()
canvas = Canvas(root, height=500)
button_placement(canvas)
canvas.pack()
canvas.mainloop()
Here's what it looks like running on my Windows system:

Categories