I am actually trying to create an application but I encountered a big issue
Specifically, it should check a folder filled with pictures for it's items and for each item it should create an ImageButton(already defined) in a Scollview GridLayout list. It works so far..
But when one of the displayed pictures is pressed, I want it to change the path of the picture on the main screen
This is my Python code
#This is outside the MDApp class
class ImageButton(ButtonBehavior, Image):
#Inside the MDApp class
def item_choser(self, item)
.
.
.
elif item == "car":
#It first clears the grid layout
self.root.ids.pictures_grid.clear_widgets()
#Then, the path folder of the items(pictures) is being defined
pictures = glob.glob(f"./images/items/cars/*.jpg")
#For loop created to assign
for i in pictures:
#This "z" is the number after path in format <path\02_test_golf7_tez.jpg>
#Different for every picture (01, 02, 03 etc)
z = i[-21:-19]
#Creating button with bind
z = ImageButton(source=i, allow_stretch=True, keep_ratio=True)
z.bind(on_release=lambda x:self.chosen(i)) <<--- Here is my actual problem
print(z)
#Adding it into the grid
self.root.ids.pictures_grid.add_widget(z)
def chosen(self, selectedCar):
print(selectedCar)
self.root.ids.main_image_display.source = selectedCar
This is what the path folder contains:
...cars\01_test_golf8_tez.jpg
...cars\02_test_golf7_tez.jpg
...cars\03_test_passat_te.jpg
...cars\04_test_crafter_t.jpg
All photos are rightly placed. For every " z " printed after the bind, it shows a different object in memory so until here everything is fine
But here is where my problem starts.
Let's say you want to pick the passat so you press the picture of it but somehow, whatever picture you press, the chosen function will always print "...cars\04_test_crafter_t.jpg"
How could this code be made in order to bind 1 button to it's function?
This was the closest answer found but either I don't understand how to use and integrate it in my code or it simply doesn't help [lambda x, i=i : i * x for i in range(len(pictures))] because it didn't work
This may not be the full answer, but I can't squeeze much code in a comment.
Make gala
a list then append each picture to it:
gala = []
for i in pictures:
z = i[-21:-19]
tmp = ImageButton(source=i, allow_stretch=True, keep_ratio=True)
tmp.bind(on_release=lambda x, i=i:self.chosen(i)) # closure hack
self.root.ids.pictures_grid.add_widget(tmp)
gala.append(tmp) # can reference later > gala[0]
Concerning the 'hack'
The target function (lamda) does not run immediately. When defining the function, python uses late binding. With this, python stores the method address (lambda) and the parameter address (i). It does not store the value of the parameter. When the function is later called, the current (latest) value of the parameter variable is used.
Take this example. When chosen
is called, i
will equal 2 because that is the value of i
when the loop is completed but before the function is actually called.
for i in range(3): 0-2
tmp.bind(on_release=lambda i:self.chosen(i)) # address of i is stored, not value
The hack resolves this issue by forcing early binding. Using i=i
tells python there is a default value for that parameter (like def myfunc(i=5)
) and early binding should be used. With early binding, the (default) parameter values are stored when the method is created, resolving the loop issue.
for i in range(3): 0-2
tmp.bind(on_release=lambda x, i=i:self.chosen(i)) # value of i is stored
The x
parameter may not be needed. You can experiment to confirm.
@inclement provided a useful link in his comment: https://docs.python-guide.org/writing/gotchas/#late-binding-closures