Using lambda function in command of Button widget in tkinter [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 2 years ago.
I'm trying to create simple GUI like this:
three set of label, entry, and READ button, one set for one row
when READ button is pressed, the value of entry will be displayed on label.
But all Read button only read from the last entry and displayed on last label.
Here is my script:
import tkinter as tk
main = tk.Tk()
label = [None]*3
entry = [None]*3
for j in range(3):
label[j] = tk.StringVar()
tk.Label(main, textvariable = label[j], relief = 'raised', width = 7).place(x = 5, y = 40+30*j)
entry[j] = tk.Entry(main, width=8)
entry[j].place(x=80, y=40 + 30 * j)
tk.Button(main, text="READ", pady=0, padx=10, command= lambda: label[j].set(entry[j].get())).place(x=150, y=40 + 30 * j)
main.mainloop()

The problem with the code you sent is that the value of j is changing with the loop, so as the loop ends, all of your buttons and lables take the value of j as 3 (thats because when your loop ends, j has the value "3") so that means all of your lables and buttons are using the last label.
An easy fix would be to manually set label[j] and entry[j] to some other variable, then apply the command.
Something like this :
lambda x=label[j], y=entry[j]: x.set(y.get())
Here I first set label[j] to x and entry[j] to y and then change the values inside lambda.
import tkinter as tk
main = tk.Tk()
label = [None]*3
entry = [None]*3
read = [None]*3
for j in range(3):
label[j] = tk.StringVar()
tk.Label(main, textvariable = label[j], relief = 'raised', width = 7).place(x = 5, y = 40+30*j)
entry[j] = tk.Entry(main, width=8)
entry[j].place(x=80, y=40 + 30 * j)
read[j] = tk.Button(main, text="READ", pady=0, padx=10, command= lambda x=label[j], y=entry[j]: x.set(y.get()))
read[j].place(x=150, y=40 + 30 * j)
main.mainloop()

Related

How to display a label after another in same row using for loop with grid in tkinter in python? [duplicate]

This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 19 days ago.
What I want:
and I can get it from long code my long code. However, I am trying to make it shorter through a loop (or anything suitable).
But I get using the code below.
Also, why do arrows disappear when I try to encapsulate all code in a function. code with long function
What I did trying to obtain desired output as above (image 1) but failed is several attempts:
from tkinter import *
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import pandas as pd
import numpy as np
tabel = pd.read_excel('testdata.xlsx', sheet_name='denied', index_col='Region')
print(tabel)
root = tk.Tk()
root.option_add("*Label*sticky", 'ew')
redDown = r"arrows/small-red-down.png"
redUp = r"arrows/small-red-up.png"
greenDown = r"arrows/small-green-down.png"
greenUp = r"arrows/small-green-up.png"
grayArrow = r"arrows/small-gray-right.png"
def arrowDenied(value=-0):
if value < -0.5:
arrow = ImageTk.PhotoImage(Image.open(greenDown))
elif value > 0.5:
arrow = ImageTk.PhotoImage(Image.open(redUp))
else: arrow = ImageTk.PhotoImage(Image.open(grayArrow))
return arrow
for i in range(4):
counter = 0
for j in range(4):
label = tk.Label(root, text=tabel.iloc[i][j]).grid(row=i,column=counter, sticky='news')
counter+=1
myimg1 = arrowDenied(tabel.iloc[i][j])
panel12 = tk.Label(root, image = myimg1).grid(row=i, column=counter)
counter+=1
root.mainloop()
I also tried replaced 'counter' with 'j+1', but that does not work. I realize the reason is when some widget is placed on 'j' the next widget will replace it because it was on the same place. I just could not figure out how to increment counter or j so that I can place arrows besides the values.
The testing data is as on this link excel file
You need to create those images (instances of PhotoImage()) only once instead of inside arrowDenied() function:
import tkinter as tk
import pandas as pd
tabel = pd.read_excel('testdata.xlsx', sheet_name='denied', index_col='Region')
print(tabel)
root = tk.Tk()
root.config(padx=10, pady=10)
#redDown = tk.PhotoImage(file="arrows/small-red-down.png")
redUp = tk.PhotoImage(file="arrows/small-red-up.png")
greenDown = tk.PhotoImage(file="arrows/small-green-down.png")
#greenUp = tk.PhotoImage(file="arrows/small-green-up.png")
grayArrow = tk.PhotoImage(file="arrows/small-gray-right.png")
def arrowDenied(value=0):
return greenDown if value < -0.5 else redUp if value > 0.5 else grayArrow
# row labels
for i, txt in enumerate(tabel.index.values):
lbl = tk.Label(root, text=txt, bg="pink", width=10, bd=1, relief="raised")
lbl.grid(row=i+1, column=0, sticky="nsew")
# column headings
for i, col in enumerate(tabel.columns.values):
lbl = tk.Label(root, text=col, bg="yellow", width=10, bd=1, relief="raised")
lbl.grid(row=0, column=i*2+1, columnspan=2, sticky="nsew")
for i in range(4):
for j in range(4):
value = tabel.iloc[i][j]
tk.Label(root, text=value).grid(row=i+1, column=j*2+1, sticky='e')
myimg1 = arrowDenied(value)
tk.Label(root, image=myimg1).grid(row=i+1, column=j*2+2, sticky='w')
root.mainloop()
Result:

Tkinter : How to pass a button's text as an argument to a function when button is clicked

I have the following code that generates a random button grid of 5x5 dimensions :
import tkinter as tk
from tkinter import *
from tkinter import messagebox
import random
def numberClick(num):
messagebox.showinfo('Message', 'You clicked the '+str(num)+' button!')
root = Tk()
#root.geometry("200x200")
w = Label(root, text='Welcome to Bingo!')
linear_array = [i for i in range(1,26)]
random_array = []
for i in range(1,26):
temp = random.choice(linear_array)
linear_array.remove(temp)
random_array.append(temp)
for i in range(25):
num = random.choice(random_array)
#tk.Label(root,text=num).grid(row=i//5, column=i%5)
redbutton = Button(root, text = num, fg ='red',height = 3, width = 5,command=lambda: numberClick(num))
redbutton.grid(row=i//5, column=i%5)
root.mainloop()
I have implemented the command function and passed a parameter with lambda as shown :
redbutton = Button(root, text = num, fg ='red',height = 3, width = 5,command=lambda: numberClick(num))
Now when I click a button , the function call should print the text value assigned with it. Instead it just prints the same value , that is the last assigned value in num variable :
Output when i clicked on button 20
Any workarounds?? TIA.
Just change your button to:
redbutton = Button(root, text = num, fg ='red',height = 3, width = 5,command=lambda num=num: numberClick(num))
This should fix the issue, this will store the value of num in lambda rather than just looping and using the last value of num.
I was just about to point out the same thing a Cool Cloud, but I'd also like to add that you are randomizing twice such that you might get duplicate numbers.
The first for loop randomizes the numbers 1-25 in random_array, but then in the second loop you randomly select one element from that list without removing it when you initialize num. I'd write the second loop as:
for i in range(25):
num = random_array[i]
redbutton = Button(root, text = num, fg ='red',height = 3, width = 5, command=lambda n=num: numberClick(n))
redbutton.grid(row=i//5, column=i%5)

Tkinter binding ListboxSelect to function with multiple Listboxes in frame [duplicate]

This question already has an answer here:
How to avoid tkinter <<ListboxSelect>> and .curselection() detecting events/selection outside of Listbox?
(1 answer)
Closed 3 years ago.
I got the idea of binding a function to a Listbox from this link:
Getting a callback when a Tkinter Listbox selection is changed?
This works very well using one Listbox only.
As soon as I tried to introduce a second Listbox on the page and also bind it to a second function the functionality like I imagined was gone.
Apparently selecting an item in the second list while an item in the first list is already selected will remove the selection from list one. Can any one help me fix that behavior?
here is my sample code:
import tkinter as tk
window = tk.Tk()
#generic window size, showing listbox is smaller than window
window.geometry("600x480")
frame = tk.Frame(window)
frame.pack()
def select(evt):
event = evt.widget
output = []
selection = event.curselection()
#.curselection() returns a list of the indexes selected
#need to loop over the list of indexes with .get()
for i in selection:
o = listBox.get(i)
output.append(o)
print(output)
def select2(evt):
event = evt.widget
output = []
selection = event.curselection()
#.curselection() returns a list of the indexes selected
#need to loop over the list of indexes with .get()
for i in selection:
o = listBox.get(i)
output.append(o)
print(output)
listBox = tk.Listbox(frame, width=20, height = 5, selectmode='multiple')
listBox.bind('<<ListboxSelect>>',select)
listBox.pack(side="left", fill="y")
scrollbar = tk.Scrollbar(frame, orient="vertical")
scrollbar.config(command=listBox.yview)
scrollbar.pack(side="right", fill="y")
listBox.config(yscrollcommand=scrollbar.set)
for x in range(100):
listBox.insert('end', str(x))
listBox2 = tk.Listbox(frame, width=20, height = 5, selectmode='multiple')
listBox2.bind('<<ListboxSelect>>',select2)
listBox2.pack(side="left", fill="y")
scrollbar2 = tk.Scrollbar(frame, orient="vertical")
scrollbar2.config(command=listBox.yview)
scrollbar2.pack(side="right", fill="y")
listBox2.config(yscrollcommand=scrollbar.set)
for x in range(100):
listBox2.insert('end', str(x))
window.mainloop()
You just need to add the parameter exportselection=False when you create your listboxes tk.Listbox(frame,...). Then your listboxes will keep being selected.
listBox = tk.Listbox(frame, width=20, height = 5, selectmode='multiple', exportselection=False)
listBox2 = tk.Listbox(frame, width=20, height = 5, selectmode='multiple', exportselection=False)

Changing color of buttons in tkinter which are created using for loop [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 5 years ago.
So I have created 5 x 5 buttons using for-loop:
for i in range(5):
for j in range(5):
gameButton = Button(boardFrame, bg="khaki1", width=10, height=5, command=colorTheButton)
gameButton.grid(row=i, column=j, sticky=E)
and I have a function called changeColor():
def colorTheButton():
global colorCounter, boardColor
row = gameButton.grid_info()['row']
column = gameButton.grid_info()['column']
if colorCounter % 2 == 0:
boardColor = "black"
else:
boardColor = "white"
colorCounter += 1
button = button(boardFrame, bg=boardColor, width=10, height=5)
button.grid(row=row, column=column)
Now the problem is, every time I click any button it'll not change the color of the clicked button instead, it'll change the color of the last created button. What should I do to change the clicked button instead of the last one?
First of all you're creating multiple buttons but they're overwritingly assigned to the very same variable, gameButton and thus you can only refer to the last button created, later on, which would be problematic.
You can instead create your buttons like:
gameButton = list()
for i in range(5)
gameButton.append(list())
for j in range(5)
gameButton[i].append(Button(boardFrame, bg="khaki1", width=10, height=5, command=colorTheButton))
gameButton[i][j].grid(row=i, column=j, sticky=E)
Secondly you should rather pass the node information to the colorTheButton method instead of trying to find which button is pressed later on:
gameButton[i].append(Button(boardFrame, bg="khaki1", width=10, height=5, command=lambda row=i, col=j : colorTheButton(row, col))
modify colorTheButton accordingly:
def colorTheButton(row, column):
global colorCounter, boardColor
if colorCounter % 2 == 0:
...
Note: Last 2 lines of that function keeps creating non-referable button objects widgets as well.
So better:
def colorTheButton(row, column):
global colorCounter, boardColor
if colorCounter % 2 == 0:
boardColor = "black"
else:
boardColor = "white"
colorCounter += 1
gameButton[row][column]['bg'] = boardColor
In your for loop, you give each new button the same name, gameButton. This means that only the most recent button is saved to the variable, in this case the bottom right button.
When you say row = gameButton.grid_info()['row'] and column = gameButton.grid_info()['column'], this therefore gets only the last buttons position in the grid. This means that only the last button is updated as you have observed.
To fix this, you could implement a list containing the buttons, or implement another way. One such way is this:
for i in range(5):
for j in range(5):
gameButton = Button(boardFrame, bg="khaki1", width=10, height=5,
command=lambda i=i, j=j: colorTheButton(i, j))
gameButton.grid(row=i, column=j, sticky=E)
I changed the command to lambda i=i, j=j: colorTheButton(i, j), which passes the actual grid location of the button to colorTheButton.
And then colorTheButton is as follows:
def colorTheButton(row, column):
global colorCounter, boardColor
if colorCounter % 2 == 0:
boardColor = "black"
else:
boardColor = "white"
colorCounter += 1
button = Button(boardFrame, bg=boardColor, width=10, height=5)
button.grid(row=row, column=column)
Note that I have tried to keep your code as similar as possible; this code stills produces a new button each time, overlaying the old one in place. This is not as efficient as modifying the original button, but that is a different question!

Refering to a button then config it

Okay, the problem Im having is that I do not understand how to refer to that particular button that was pressed. Im doing a game, if the use clicks a button I wanna know which X and Y coordinate it had. So, if you have a grid of buttons and you click one I want the coordinates and then I will change that button's color. :)
PROBLEM: Knowing which button was pressed in a grid.
Thanks in advance!
def matris():
for i in range(5):
newButton = Button(app, width = 4, height = 2, bg = "blue",command = lambda i=i: function(i))
newButton.grid(row = i, column = 0)
for i in range(5):
newButton = Button(app, width = 4, height = 2, bg = "blue",command = lambda i=i + 5: function(i))
newButton.grid(row = i, column = 1)
for i in range(5):
newButton = Button(app, width = 4, height = 2, bg = "blue",command = lambda i=i + 10: function(i))
newButton.grid(row = i, column = 2)
for i in range(5):
newButton = Button(app, width = 4, height = 2, bg = "blue",command = lambda i=i + 15: function(i))
newButton.grid(row = i, column = 3)
for i in range(5):
newButton = Button(app, width = 4, height = 2, bg = "blue",command = lambda i=i + 20: function(i))
newButton.grid(row = i, column = 4)
def function(i):
if button 23 was clicked.changeColor to e.g "blue"
After reading all the comments and edits, it looks like what you really want to know is "which button was clicked?" when using the same command for multiple buttons.
The easiest way to do that is to pass some sort of unique identifier to the command associated with the widget. The most straight-forward way is to pass a reference to the widget itself, though that requires a two step process.
For example:
this_button = Button(...)
this_button.configure(command=lambda button=this_button: do_something(button))
def do_something(button):
print "you clicked this button:", button
You can also use functools.partial to get the same result if you find lambda hard to wrap your head around:
this_button.configure(command=functools.partial(do_something, this_button)
If you prefer to create your button in a single step instead of two, you need some way to identify it. For me, the easiest way is with a dictionary. For example, if you're creating rows and columns of buttons, you could do this:
button = {}
for r in range(10):
for c in range(10):
button[r,c] = Button(..., command=lambda row=r, column=c: do_something(row, ccolumn))
def do_something(row, column):
print "you clicked button", button[row,column]
You need to keep a reference to each button if you want to modify that button later. Since your numbering scheme is sequential you can use a list:
newButton = Button(...)
buttons.append(newButton)
...
def function(i):
widget = buttons[i-1] # -1, because list indexes are zero-based
if i == 23:
widget.configure(background="blue")
If you're creating a grid of buttons and the buttons are otherwise identical, you might want to consider a simpler structure such as:
for row in range(5):
for column in range(5):
...
It becomes instantly clear that you're creating five rows of five columns, whereas with your original code it takes several seconds of study to come to the same conclusion.
Instead of using the command argument, use the bind method to set a callback for <Button-1>. When Tkinter calls your callback, it will pass in an event object, which contains the widget that raised the event.
from Tkinter import *
def buttonClicked(e):
e.widget["bg"] = "red"
root = Tk()
for x in range(5):
for y in range(5):
newButton = Button(root, width=10, height=2, bg="blue")
newButton.bind("<Button-1>", buttonClicked)
newButton.grid(row=y, column=x)
root.mainloop()
Edit: suppose you want to treat buttons differently depending upon their position in the grid. The simplest way to do this is to use a global dictionary to associate each button with its coordinate.
from Tkinter import *
coords = {}
def buttonClicked(e):
x,y = coords[e.widget]
print "{},{} clicked".format(x,y)
if x == 4 and y == 3:
e.widget["bg"] = "red"
root = Tk()
for x in range(5):
for y in range(5):
newButton = Button(root, width=10, height=2, bg="blue")
newButton.bind("<Button-1>", buttonClicked)
newButton.grid(row=y, column=x)
coords[newButton] = (x,y)
root.mainloop()
In general, however, having a variable at the global scope isn't a good idea. It may be worth the effort to group all of your grid-based code into a single class, so none of its details leak out to the rest of the program.
from Tkinter import *
class ButtonGrid:
def __init__(self, root):
self.coords = {}
for x in range(5):
for y in range(5):
newButton = Button(root, width=10, height=2, bg="blue")
newButton.bind("<Button-1>", self.buttonClicked)
newButton.grid(row=y, column=x)
self.coords[newButton] = (x,y)
def buttonClicked(self, e):
x,y = self.coords[e.widget]
print "{},{} clicked".format(x,y)
if x == 4 and y == 3:
e.widget["bg"] = "red"
root = Tk()
b = ButtonGrid(root)
root.mainloop()
If you prefer functional over object-oriented, here's an alternative. Use the command option as you did in your original code, but use the functools.partial function to specify ahead of time which variables ought to be passed to the function.
from Tkinter import *
import functools
def buttonClicked(widget, x, y):
print "{},{} clicked".format(x,y)
if x == 4 and y == 3:
widget["bg"] = "red"
root = Tk()
for x in range(5):
for y in range(5):
newButton = Button(root, width=10, height=2, bg="blue")
newButton["command"] = functools.partial(buttonClicked, newButton, x, y)
newButton.grid(row=y, column=x)
root.mainloop()

Categories

Resources