Kivy : how to get widget by id (without kv) - python

Let's say I define on the fly in Kivy a few widgets (buttons) and dynamically assign their id.
I'm not using kv language in this use case.
I can keep a reference of a widget id without keeping track of the widget itself : then I'd like to access the widget through its id.
Can I do something like "get widget by id" ?
(If I had defined the widget in a kv file, I could have used self.ids.the_widget_id to access the widget itself through its id)

Kivy widgets make tree structure. Children of any widget are avaiable through children atribute. If you want, you can keep reference only to root window and then iterate over it's widgets using walk method:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class MyWidget(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
button = Button(text="...", id="1")
button.bind(on_release=self.print_label)
l1 = BoxLayout(id="2")
l2 = BoxLayout(id="3")
self.add_widget(l1)
l1.add_widget(l2)
l2.add_widget(button)
def print_label(self, *args):
for widget in self.walk():
print("{} -> {}".format(widget, widget.id))
class MyApp(App):
def build(self):
return MyWidget()
if __name__ == '__main__':
MyApp().run()
walk() and walk_reverse() method were added to kivy.uix.widget.Widget in 1.8.1 version of Kivy. For older versions you need to recursively parse tree yourself:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class MyWidget(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
button = Button(text="...", id="1")
button.bind(on_release=self.print_label)
l1 = BoxLayout(id="2")
l2 = BoxLayout(id="3")
self.add_widget(l1)
l1.add_widget(l2)
l2.add_widget(button)
def print_label(self, *args):
children = self.children[:]
while children:
child = children.pop()
print("{} -> {}".format(child, child.id))
children.extend(child.children)
class MyApp(App):
def build(self):
return MyWidget()
if __name__ == '__main__':
MyApp().run()

You can retrieve the widget using directly the ids. For example in your code you can modify the Button text with the following snippet:
self.ids.2.ids.3.ids.1.text = '!!!!'

You can change properties of each widget using ids:
self.ids['order_number'].text='S1212'

Related

Create widgets based on user input, in the .kv file

I want to ask the user for a number, then display that amount of widgets.
This is how I do it:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
class MainApp(App):
def build(self):
return List()
class List(GridLayout):
def __init__(self, **kwargs):
super(List, self).__init__(**kwargs)
self.cols = 1
user_input = 3 # Just an example
for i in range(user_input):
label = Label(text="Widget number {}:".format(i + 1))
self.add_widget(label)
if __name__ == "__main__":
app = MainApp()
app.run()
The problem is: the point of "kivy language" is to keep the logic in the .py file, and the design in the .kv file.
Is there any way I can keep the design only in .kv file, while doing this (having a for loop)?
(I'm new to kivy, sorry if I'm asking a simple question. :) )
Definitely, you can.
The KV file is used primarily to design the looks while the login part of a program is handled by the PY file.
.kv file
<classname>:
Button:
text:'click me'
on_release: root.function_to_call_from_py_file()
.py file
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
class List(GridLayout):
def function_to_call_from_py_file(self):
print('This function is called')
class MainApp(App):
def build(self):
return List()
if __name__ == "__main__":
app = MainApp()
app.run()

How to Bind on_press to GridLayout in kivy python

i was trying to bind on_press method to gridLayout in kv language but i got this error AttributeError: pressed. I did some research even in the kivy doc but no help .So if any one has a solution to this problem please you may be a good resource
here is my testApp.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
class screendb(BoxLayout):
def mycall_back(self):
print('hello')
class testApp(App):
def build(self):
return screendb()
if __name__=='__main__':
testApp().run()
here is my testApp.kv
<Screendb#BoxLayout>:
GridLayout:
on_pressed:root.Mycall_back()
In your py file:
# Behaviors let you add behavior from one widget to another widget
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.gridlayout import GridLayout
# We create a new widget that is a GridLayout with access to Button Behaviors
# Like Button's 'on_press' method
class ButtonGrid(ButtonBehavior, GridLayout):
pass
class screendb(BoxLayout):
def mycall_back(self):
print('hello')
class testApp(App):
def build(self):
return screendb()
In your kv file:
# We create a Root Widget for the ButtonGrid
<ButtonGrid>:
<Screendb#BoxLayout>:
# We add an instance of the ButtonGrid class to our main layout
ButtonGrid:
# We can now use on_press because it is a part of the ButtonBehavior class, that makes up your ButtonGrid class.
on_press:root.mycall_back()
Note: There were a few minor mistakes in your post as well. For example, there is no method 'on_pressed', only 'on_press', you also wrote your callback as 'mycall_back' in your py file while writing it as 'Mycall_back' in your kv file, which refers to a different method that exists. Make sure your letter cases match.
video example

Kivy: Kivy launcher fall down

I have simple code for kivy, on W10 runs without problem. It falls down during loading in kivy launcher. Problem is without message.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class View(BoxLayout):
def __init__(self):
super().__init__()
self.text = "No text"
but = Button(text = "Press",on_press = self.show)
self.add_widget(but)
self.lbl = Label()
self.add_widget(self.lbl)
def show(self,obj):
self.lbl.text = self.text
pass
class MyPaintApp(App):
def build(self):
return View()
if __name__ == '__main__':
MyPaintApp().run()
It does not run because you call super wrong.
As kivy launcher uses python 2, you need to pass your class (View) and the instance (self) to super.
You need to edit your class like this:
class View(BoxLayout):
def __init__(self,**kwargs):
super(View,self).__init__(**kwargs)
in every failure in kivy launcher, there is a '.kivy/log' directory inside the project directory that has a full log. you could find all the problem there.

Adding a new Button on release of another button in Kivy

I'm trying to have a button where if it is clicked it will insert a new button. I cant get the bind to return the new button.
from kivy.base import runTouchApp
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.uix.scatter import Scatter
from kivy.uix.button import Button
class app(App):
def build(self):
layout = FloatLayout()
button1 =Button(text="test",pos=(385,450),size_hint=(.1,.1))
button1.bind(on_release=self.btn2)
layout.add_widget(button1)
return layout
def btn2(self, event):
print "worked"
layout = FloatLayout()
btn3 = Button(text="worked",size=(.1,.1),pos=(380,400))
layout.add_widget(btn3)
return layout
app().run()
You are creating another instance of FloatLayout in the btn2 event with layout = FloatLayout(), however that instance isn't anywhere else, but in the btn2 method - i.e. you added a Button to FloatLayout, but that layout isn't visible and after the function ends it's highly possible that even doesn't exist, because it's garbage-collected by Python.
You can either use partial to pass the already existing instance of FloatLayout (where your previous Button is) like this if you need only one layout:
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from functools import partial
class app(App):
def build(self):
layout = FloatLayout()
button1 =Button(text="test",pos=(385,450),size_hint=(.1,.1))
button1.bind(on_release=partial(self.btn2, layout))
layout.add_widget(button1)
return layout
def btn2(self, layout, *args):
print "worked"
btn3 = Button(text="worked",size=(.1,.1),pos=(380,400))
layout.add_widget(btn3)
app().run()
Or you can use the instance that's passed to method arguments from the event (the instance of a widget that dispatched the event):
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
class app(App):
def build(self):
layout = FloatLayout()
button1 =Button(text="test",pos=(385,450),size_hint=(.1,.1))
button1.bind(on_release=self.btn2)
layout.add_widget(button1)
return layout
def btn2(self, button):
layout = button.parent # parent of the button is the "layout" from build()
btn3 = Button(text="worked",size=(.1,.1),pos=(380,400))
layout.add_widget(btn3)
app().run()
In both cases you only need to find the right instance to work with, not create another one that's not even used. Also return layout isn't necessary if you don't expect on_release to work with the layout variable (which it won't).

Binding Kivy ObjectProperty to a child widget doesn't seem to work outside of root widget

Trying to follow this guide : https://kivy.org/docs/guide/lang.html#accessing-widgets-defined-inside-kv-lang-in-your-python-code
I am attempting to access a widget using an id definition. This works well inside the root widget but it doesn't seem to work outside of it.
As an example here's a bare minimum code representing my issue :
GUI.kv file :
<PlotBox#BoxLayout>:
graph2:graph2_id
BoxLayout:
id:graph2_id
<RootWidget#BoxLayout>:
graph:graph_id
BoxLayout:
id:graph_id
PlotBox:
python file :
#kivy imports
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
class PlotBox(BoxLayout):
graph2 = ObjectProperty(None)
def __init__(self,**kwargs):
super(PlotBox,self).__init__(**kwargs)
self.graph2.add_widget(Button(text="This doesn't work"))
class RootWidget(BoxLayout):
graph = ObjectProperty(None)
def __init__(self,**kwargs):
super(RootWidget,self).__init__(**kwargs)
self.graph.add_widget(Button(text='This works'))
class GUIApp(App):
def build(self):
self.root = RootWidget()
return self.root
if __name__ == "__main__":
GUIApp().run()
I get the error :
AttributeError: 'NoneType' object has no attribute 'add_widget'
On the RootWidget, it works even if I don't use graph = ObjectProperty(None).
On my other widget, it's like the id doesn't get created.
According to the docs:
The # character is used to separate your class name from the classes you want to subclass. [...]
From what is concluded that it is an equivalent way to do inheritance in the .kv similar to python so you should only select one of those ways. That causes PlotBox from .py to never be invoked.
Another error, according to the docs, I do not know if it's your error but the .kv must be gui.kv, with lowercase.
children are not loaded directly after executing the parent's constructor so adding it in the constructor can generate problems, a recommendation and a very common practice in kivy is to use Clock.
All the above I have implemented in the following codes:
gui.kv
<PlotBox>:
graph2:graph2_id
BoxLayout:
id:graph2_id
<RootWidget>:
graph:graph_id
BoxLayout:
id:graph_id
PlotBox:
main.py
#kivy imports
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
from kivy.clock import Clock
class PlotBox(BoxLayout):
graph2 = ObjectProperty(None)
def __init__(self,**kwargs):
super(PlotBox,self).__init__(**kwargs)
Clock.schedule_once(lambda dt: self.graph2.add_widget(Button(text="This now works")))
class RootWidget(BoxLayout):
graph = ObjectProperty(None)
def __init__(self,**kwargs):
super(RootWidget,self).__init__(**kwargs)
self.graph.add_widget(Button(text='This works'))
class GUIApp(App):
def build(self):
root = RootWidget()
return root
if __name__ == "__main__":
GUIApp().run()
Output:
I think self.graph2 just hasn't been set yet during the __init__ - the __init__ has to return before any children can be added.
You can work around this by doing something like Clock.schedule_once(function_that_adds_the_button, 0).
I'm working under the assumption that you want this code to run when the app is being created, no later.
kv.
<PlotBox>:
BoxLayout:
id:graph2_id
<RootWidget>:
BoxLayout:
id:graph_id
PlotBox:
id: plot
py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class PlotBox(BoxLayout):
pass
class RootWidget(BoxLayout):
pass
class GUIApp(App):
def build(self):
root = RootWidget()
# We can add things to the Root during build before we return it
# This means we add this information before the user sees anything
root.ids.graph_id.add_widget(Button(text='This works'))
# See end of answer for more about this
root.ids.plot.ids.graph2_id.add_widget(Button(text='This works!'))
return root
if __name__ == "__main__":
GUIApp().run()
Firstly, you don't need Object Properties to access ids, you can do it through ids or children:
self.ids.IDGOESHERE
OR
self.children[INDEXOFIDGOESHERE]
As for this line:
root.ids.plot.ids.graph2_id.add_widget(Button(text='This works!'))
Root has an instance of the plotbox class with the id 'plot'. The Plot class (and therefore all instances of the plot class) have an instance of the BoxLayout with the id graph that we can access.
So what we're doing is:
Root -> Plot -> Graph2
If we were to add another plotbox with the id 'big_plot', then we could do either what we did before to get one Graph2 or we could get a different graph2 because it belongs to a different instance of the plotbox.
What we did before
Root -> Plot -> Graph2
A different id, therefore a different widget.
Root -> big_plot -> Graph2
Unless you're invoking super, you'll rarely need to use the init method in a Kivy Widget Class (or so in my experience anyway).
Edit:
If you're going to access super long addresses repeatedly, you can wrap them in a function to get them.
example:
Not great:
def func_one(self):
newtext = 'new'
self.ids.IDONE.ids.IDTWO.ids.IDTHREE.ids.IDFOUR.text = newtext
def func_two(self):
newtext = 'newtwo'
self.ids.IDONE.ids.IDTWO.ids.IDTHREE.ids.IDFOUR.text = newtext
def func_three(self):
newtext = 'newthree'
self.ids.IDSONE.Ids.IDTWO.ids.IDTHREE.ids.IDFOUR.text = newtext
Better:
def long_address(self):
return self.ids.IDSONE.ids.IDSTWO.ids.IDTHREE.ids.IDFOUR
def func_one(self):
newtext = 'new'
self.long_address().text = newtext
def func_two(self):
newtext = 'newtwo'
self.long_address().text = newtext
def func_three(self):
newtext = 'newthree'
self.long_address().text = newtext

Categories