Posts: 158
Threads: 27
Joined: Jan 2019
I'm looking to create buttons that remember where they are are in a 2D list. This way they can share a single function that takes their 2D list indices as parameters. Ultimately, I want the button to be able to manipulate its own text without having to have a separate reference for each button outside of the list and without having to have a separate function associated with each button.
The buttons display properly with the correct text, but the lambdas reference only the final values of grid_row and grid_column, so all buttons print "3, 3" when pressed.
from tkinter import *
root = Tk()
root.title(" ")
root.geometry("250x250+0+20")
def button_print(row, column):
print(f"{row + 1}, {column + 1}")
buttons = []
for grid_row in range(3):
button_row = []
for grid_column in range(3):
button = Button(root, text = f"{grid_row + 1} {grid_column + 1}",
command = lambda: button_print(grid_row, grid_column), width = 4, height = 3)
button.grid(row = grid_row, column = grid_column, padx = (10, 10), pady = (10, 10))
button_row.append(button)
buttons.append(button_row)
Posts: 2,125
Threads: 11
Joined: May 2017
With a trick, you could use command with partial, to call the callback together with the instance of the button itself as the first argument.
Example:
from tkinter import Tk, Button
from tkinter.messagebox import showinfo
from itertools import product
from functools import partial
class Gui(Tk):
def __init__(self):
super().__init__()
self.setup()
self.selected: None | Button = None
def setup(self):
for number, (col, row) in enumerate(product(range(10), range(5))):
button = Button(self, text=f"Button {number:02d}")
# callback, which also submits the button innstance to the function
button["command"] = partial(self.clicked, button)
button.grid(row=row, column=col)
def clicked(self, button: Button):
print(button["text"])
if self.selected:
self.selected, selected = None, self.selected
grid1 = button.grid_info()
grid2 = selected.grid_info()
button.grid_configure(row=grid2["row"], column=grid2["column"])
selected.grid_configure(row=grid1["row"], column=grid1["column"])
showinfo(
"Button position changed",
f"{selected['text']} swapped with {button['text']}",
)
else:
self.selected = button
if __name__ == "__main__":
Gui().mainloop()
Clunk_Head likes this post
Posts: 158
Threads: 27
Joined: Jan 2019
(Jun-21-2023, 05:09 PM)DeaD_EyE Wrote: With a trick, you could use command with partial, to call the callback together with the instance of the button itself as the first argument.
Thank you.
Looks like I can't get there from here without reinventing the wheel. Guess that rules out tkinter. Is there a GUI alternative that is natively capable of achieving my goal?
Posts: 4,790
Threads: 76
Joined: Jan 2018
Jun-21-2023, 06:54 PM
(This post was last modified: Jun-21-2023, 06:54 PM by Gribouillis.)
You could also store the row and column as attributes in the Button instance
from functools import partial
from tkinter import *
root = Tk()
root.title(" ")
root.geometry("250x250+0+20")
def button_print(button):
print(f"{button.row + 1}, {button.column + 1}")
buttons = []
for grid_row in range(3):
button_row = []
for grid_column in range(3):
button = Button(root, text = f"{grid_row + 1} {grid_column + 1}", width = 4, height = 3)
button.row = grid_row
button.column = grid_column
button.configure(command=partial(button_print, button))
button.grid(row = grid_row, column = grid_column, padx = (10, 10), pady = (10, 10))
button_row.append(button)
buttons.append(button_row)
root.mainloop() (Jun-21-2023, 06:29 PM)Clunk_Head Wrote: Is there a GUI alternative that is natively capable of achieving my goal? I think all GUIs including tkinter allow you to attach client data to a widget and to retrieve this client data when a callback is invoked.
Clunk_Head likes this post
Posts: 158
Threads: 27
Joined: Jan 2019
(Jun-21-2023, 06:54 PM)Gribouillis Wrote: You could also store the row and column as attributes in the Button instance
That's fantastic, but both solutions provided had a commonality that ended up being the key to the solution that I most desired:
from functools import partial
from tkinter import *
root = Tk()
root.title(" ")
root.geometry("250x250+0+20")
def button_print(row, column):
print(f"{row + 1}, {column + 1}")
buttons = []
for grid_row in range(3):
button_row = []
for grid_column in range(3):
button = Button(root, text = f"{grid_row + 1} {grid_column + 1}", width = 4, height = 3,
command = partial(button_print, grid_row, grid_column))
button.grid(row = grid_row, column = grid_column, padx = (10, 10), pady = (10, 10))
button_row.append(button)
buttons.append(button_row)
root.mainloop() The use of partial returned a unique function with the parameters hard wired, which is what I failed to do with the lambda.
Thank you both very much for your help.
I guess the last question for completeness, did my first code create one lambda that was referenced by all buttons, or did it crate a lambda for each function that all referenced the same variables?
Posts: 4,790
Threads: 76
Joined: Jan 2018
Jun-22-2023, 06:18 AM
(This post was last modified: Jun-22-2023, 06:21 AM by Gribouillis.)
(Jun-21-2023, 08:20 PM)Clunk_Head Wrote: did my first code create one lambda that was referenced by all buttons, or did it crate a lambda for each function that all referenced the same variables? It created a lambda for each button that all referenced to the same variables as in this example
>>> def func():
... var = 3
... spam = lambda: var
... var = 4
... ham = lambda: var
... return spam, ham
...
>>> f, g = func()
>>> f is g
False
>>> f.__closure__
(<cell at 0x7f3135163160: int object at 0x7f3135374150>,)
>>> g.__closure__
(<cell at 0x7f3135163160: int object at 0x7f3135374150>,)
>>> f.__closure__[0].cell_contents
4
>>> g.__closure__[0].cell_contents
4
>>>
Clunk_Head likes this post
Posts: 6,798
Threads: 20
Joined: Feb 2020
When you use a variable in a lambda expression, the expression creates a closure, an expression, and the relevant context required to evaluate the expression. In this example, the lambda expression "p" creates a closure that contain the variables x and y.
Output: Python 3.10.7 (tags/v3.10.7:6cc6b13, Sep 5 2022, 14:08:36) [MSC v.1933 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 0
>>> y = 0
>>> p = lambda: print(x, y)
>>> p()
0 0
>>> x = 2
>>> p()
2 0
>>> y = 3
>>> p()
2 3
A partial function does something different. When you build a partial function, any variables passed to the constructor are evaluated, and the value is passed to the constructor (just like any Python function call).
Output: Python 3.10.7 (tags/v3.10.7:6cc6b13, Sep 5 2022, 14:08:36) [MSC v.1933 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from functools import partial
>>> x = 0
>>> y = 1
>>> p = partial(print, x, y)
>>> p()
0 1
>>> x = 10
>>> y = 42
>>> p()
0 1
>>>
The partial never knew about x or y. The partial() constructor was passed 0 and 1, not x and y.
Clunk_Head likes this post
|