So I have written this basic kivy code. I want to read sentences from a file, and dynamically create buttons for each one of them. Then, I want these buttons to disappear when they are clicked. In a for loop, I create my buttons, and put my buttons with the index i in a list. Then with the on_press method, it should delete itself.
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
So there is a Button in Button_List[i] , and when it is clicked, it should run:
self.remove_widget(Button_List[i])
so it should delete itself
I have 5 buttons for example, the problem is, that it whichever button I click, it deletes the button with the highest index. And the other buttons dont get deleted. I feel like kivy is only executing the last index, but I am not sure.
Here is my code:
new.py:
import kivy.uix.button as kb
from kivy.app import App
from kivy.uix.widget import Widget
from Nils_Programm_verkürzt import lektionstextlesen
from kivy.uix.gridlayout import GridLayout
sentences = ['example_sentence1','example_sentence2','example_sentence3','example_sentence4','example_sentence5',]
class Button_Widget(Widget):
def __init__(self, **kwargs):
super(Button_Widget, self).__init__(**kwargs)
global Button_List
Button_List = []
for i in range(len(sentences)):
print(i)
Button_List.append(kb.Button(text=sentences[i],pos=(self.width * i, self.height * i)))
Button_List[i].size = 50, 50
print('binding'+ str(i))
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
print(Button_List[i])
self.add_widget(Button_List[i])
class ButtonApp(App):
def build(self):
return Button_Widget()
if __name__ == "__main__":
ButtonApp().run()
Your help is very much appreciated :).
That's a common problem when using lambda in a loop. They all get the last value of the loop. A fix is to create a new variable that holds the current loop variable value. So, try replacing:
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
with:
Button_List[i].bind(on_press= lambda x, j=i: self.remove_widget(Button_List[j]))
Related
In a Kivy/Python program, I have a class which contains a scrollview. I instantiate this class multiple times. The scrollview is bound to the on_scroll_stop event. My desired outcome is that the on_scroll_stop event will only fire within the instantiated class to which it belongs, but it seems that the event fires across all of the instantiated classes. Am I doing something wrong or is this expected behavior? Working sample below. In this example the "Left" section shows the issue most often, however the error is seen in the right section once you scroll up at the top or scroll down once reaching the bottom. To recreate scroll only one side, preferably the "Left" side. In my actual code which is far more complex the issue is much more prevalent.
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
class DisplaySection(Widget):
def CreateSection(self):
self.display_panel_main_layout = BoxLayout(orientation='vertical')
self.elements_layout = FloatLayout(size_hint_y=None)
self.sv = ScrollView()
self.sv.bind(on_scroll_stop=self.on_scrolling_stop)
self.sv.add_widget(self.elements_layout)
self.display_panel_main_layout.add_widget(self.sv)
return self.display_panel_main_layout
def LoadElements(self,section_name):
self.section_name = section_name
number_of_elemements = 100
element_hieght = 40
layout_height = number_of_elemements * element_hieght
self.elements_layout.height = layout_height
xcord = 0
for x in range(number_of_elemements):
ycord = self.elements_layout.height - element_hieght*x
name = Label(text='Name' + str(x),size_hint_y=None, height=40,pos=(xcord,ycord))
self.elements_layout.add_widget(name)
def on_scrolling_stop(self, sv, value):
#print(sv,value)
print('I am',self.section_name)
class testscrollapp(App):
def build(self):
main_layout = BoxLayout()
section_names = ['Left','Right']
for x, section_name in enumerate(section_names):
section_layout = BoxLayout(orientation='vertical')
btn = Button(text=section_name,size_hint=(1,None))
section_layout.add_widget(btn)
# instantiate the class containing the scroll view
scroll_layout = DisplaySection()
scr = scroll_layout.CreateSection()
section_layout.add_widget(scr)
scroll_layout.LoadElements(section_name)
main_layout.add_widget(section_layout)
return main_layout
testscrollapp().run()
The on_scroll_stop event is dispatched just like touch events, so any ScrollView instance that subscribes to on_scroll_stop will get all the on_scroll_stop events. And just like a touch event, the subscribing ScrollView instance must determine which of the events it is interested in, and ignore the rest. Since the value argument to your on_scrolling_stop is the MouseMotionEvent, you can do a collide_point() test to determine if the scroll event is within the ScrollView:
def on_scrolling_stop(self, sv, value):
if sv.collide_point(*value.pos):
print('\tI am',self.section_name)
I need a function to count how many buttons were clicked in a group of maybe three of four buttons. That is if I click on two buttons out of the three or four buttons, it should print out you clicked two buttons
import kivy
from kivy.app import App
from kivy.uix.floatlayout import Floatlayout
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.label import Label
class LandingScreen(FloatLayout):
def __init__(self, **kwargs):
super(LandingScreen, self).__init__(**kwargs)
# put whatever pos_hint value you want and the on_press event.
self.btn1=Button(text='button1 ', size_hint=(0.5, 0.5)))
self.btn2=Button(text='button1 ', size_hint=(0.5, 0.5)))
self.btn3=Button(text='button1 ', size_hint=(0.5, 0.5)))
self.add_widget(self.btn1)
self.add_widget(self.btn2)
self.add_widget(self.btn3)
# I need a function to count the numbers of buttons clicked out of the three buttons
class SplashApp(App):
def build(self):
return LandingScreen()
if __name__ == '__main__':
SplashApp().run()
When the button gets pressed, it should trigger a function. This function should add a unique ID to a set. This unique ID can be 1 for the first button, 2 for the second, etc.
It might look like this:
mySet = set()
def onClick(self, id):
mySet.add(id)
#bind the function to the button
self.btn1.bind(on_press = self.onClick(1))
self.btn2.bind(on_press = self.onClick(2))
self.btn3.bind(on_press = self.onClick(3))
To get how many buttons were pressed, you can then just get the length by doing len(mySet).
Why use a set over a list?
A list can have multiple of the same item. A set must be unique. If a uesr clicks the button 2 times, the list could look like this: [1, 2, 2]. A set, despite how many clicks, will look like this: [1, 2]
I am trying to figure out a way to index which button was pressed in a GridLayout so that, for instance, I can put a specific image in that button's background when it is pressed. Here is what I am currently doing, using a function just to try to print the index number as a test before adding more functionality:
for x in range(15):
self.buttons.append(Button())
self.ids.grid_1.add_widget(self.buttons[x])
self.buttons[x].background_normal = 'YOUTUBE.png'
self.buttons[x].background_down = 'opacity.png'
# Make the button switch screens to input from calling the function above
if edit_mode is True:
self.buttons[x].bind(on_release=self.SwitchScreenInput)
self.buttons[x].bind(on_release=self.HoldButtonNum(x))
def HoldButtonNum(x):
print(x)
Im getting the error:
TypeError: HoldButtonNum() takes 1 positional argument but 2 were
given
Process finished with exit code 1
I will make some observations:
If HoldButtonNum is an instance method its first parameter must be self.
You must use functools.partial or lambda functions to pass arguments to event handler.
The function must receive a third parameter which is the instance of the button that launches the event.
An example:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from functools import partial
class MyGridLayout(GridLayout):
cols = 5
def __init__(self):
super(MyGridLayout, self).__init__()
self.buttons = []
for x in range(15):
self.buttons.append(Button())
self.add_widget(self.buttons[x])
self.buttons[x].bind(on_release=partial(self.HoldButtonNum, x))
def HoldButtonNum(self, x, instance):
print('Button instance:', instance)
print('Button index in list:', x)
class MyKivyApp(App):
def build(self):
return MyGridLayout()
def main():
app = MyKivyApp()
app.run()
if __name__ == '__main__':
main()
When a button is pressed the output is like:
Button index in list: 1
Button instance: <kivy.uix.button.Button object at 0x0000018C511FC798>
I want to iterate through a list of button and bind each button with a different function. But the result is always the last Button's function, not all in the list.
Here's my code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.stacklayout import StackLayout
from kivy.uix.button import Button
class MyApp(App):
def build(self):
parent = StackLayout()
button_list = []
for i in range(0,11):
button_list.append( Button(text = str(i), size_hint = (None,0.15)) )
print i
for a_button in button_list:
parent.add_widget(a_button)
a_button.bind( on_press = lambda x: parent.add_widget( Label(text = a_button.text) ) )
return parent
if __name__ == "__main__":
MyApp().run()
There is some problem with my lambda function, I guessed.
This is a classic python problem, not actually related to kivy. It is discussed e.g. here.
For those still running into this problem 6 years later, here is my solution:
def buttongenerator(layout):
hellodict = {}
for k in range(50):
hellodict[str(k)] = 'Hello' + str(k)
btn = Button(text=str(k),
size_hint_y=None,
height='40dp')
btn.bind(on_release=(lambda instance, var=hellodict[str(k)]: print(var)))
layout.add_widget(btn)
Layout simply refers to a grid/box layout.
The instance variable is necessary because otherwise the button object will be passed into the lambda function and (in this case) will be printed to the console as a string. The instance variable sort of "absorbs" the button object, if that makes any sense. (The variable does not have to be named instance, it just has to be present)
I have an app that needs to add lots of widgets dynamically. Here's a working app to simulate this:
from threading import Thread
from kivy.app import App
from kivy.uix.stacklayout import StackLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.clock import Clock
class LotsOfWidgets(App):
def build(self):
self.widgets_amt = 5000
root = GridLayout(cols=1)
self.label_container = StackLayout()
generate_button = Button(text='Generate lots of labels',
size_hint_y=None, height=44)
generate_button.bind(on_press=self.generate_lots_of_labels)
hooray_button = Button(text='Print hooray',
size_hint_y=None, height=44)
hooray_button.bind(on_press=self.print_hooray)
for widget in (generate_button, hooray_button,
self.label_container):
root.add_widget(widget)
return root
def generate_lots_of_labels(self, *args):
for _ in xrange(self.widgets_amt):
label = Label(text='a', size_hint=(None, None), size=(10,10))
self.label_container.add_widget(label)
def scheduled_generate_lots_of_labels(self, *args):
Clock.schedule_once(self.generate_lots_of_labels)
def threaded_generate_lots_of_labels(self, *args):
thread = Thread(target=self.generate_lots_of_labels)
thread.start()
def print_hooray(self, *args):
print 'hooray'
LotsOfWidgets().run()
We have a grid layout that has 2 buttons and a stack layout. By clicking the first button, 5000 labels will be generated inside the stack layout. The second button only prints "hooray" to the console.
Adding 5000 widgets to the stack layout and drawing them on the screen takes quite a bit of time which is fine. When you press the button to generate them and immediately press the "print hooray" button, on my computer hooray gets printed about 3 seconds later after the labels appear on the screen. So the problem is, that the UI becomes unresponsive while generating the labels.
I tried to solve this with threading by binding generate_button.on_press to scheduled_generate_lots_of_labels and threaded_generate_lots_of_labels (of course not at the same time) methods instead the one shown in the code, but they don't seem to help.
Is there anything you can do to keep the UI responsive even if it's generating all those widgets?
You could add the labels in batches scheduled by Kivy's Clock module.
I looked into the idea that brousch gave in his answer. What I tried first was to split those 5000 items into smaller chunks and iterating each chunk in its own Clock.schedule_once.
The result turned out to be very similar to just scheduling once the whole shebang of 5000 items. If you schedule it to be executed in 1 second, you have 1 second to click the hooray button. After that UI becomes unresponsive until all the widgets have been generated.
So in most cases, the only option is to use Clock.schedule_interval and here's an experiment of that. The build method remains the same.
def chunk(self, l, n):
for i in xrange(0, len(l), n):
yield l[i:i+n]
def generate_lots_of_labels(self, *args):
n = 500
interval = 0.01
self.chunks = list(self.chunk(range(self.widgets_amt), n))
self.i = 0
Clock.schedule_interval(self.iterate, interval)
def iterate(self, *args):
for _ in self.chunks[self.i]:
label = Label(text='a', size_hint=(None, None), size=(10,10))
self.label_container.add_widget(label)
self.i += 1
if self.i >= len(self.chunks):
Clock.unschedule(self.iterate)
This is a compromise between the widget generation speed and responsiveness of the UI. Depending on the application and executing environment, different values of scheduling interval and n give the best result.