Python Forum
[Tkinter] Programmatically creating buttons that remember their positions
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Programmatically creating buttons that remember their positions
#1
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)
Reply
#2
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
Almost dead, but too lazy to die: https://sourceserver.info
All humans together. We don't need politicians!
Reply
#3
(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?
Reply
#4
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
Reply
#5
(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?
Reply
#6
(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
Reply
#7
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
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Tkinter] How to add conversions and fix button positions. javesike1262 7 2,906 Jan-31-2021, 04:39 PM
Last Post: deanhystad
  Creating a frame with 4 command buttons Heyjoe 5 2,485 Aug-21-2020, 03:16 PM
Last Post: deanhystad
  GUI Tkinter Widget Positions punksnotdead 3 2,976 Jun-12-2019, 06:06 PM
Last Post: Yoriz
  A little idea to remember wxPython classes Sebastian_Adil 0 2,318 Mar-26-2018, 10:23 PM
Last Post: Sebastian_Adil
  Fill out form on webpage and post request programmatically ian 2 3,626 Jul-18-2017, 03:12 PM
Last Post: ian

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020