Updating Kivy Widgets when Root Object Changes - python

I have a data structure in the form of an object that I am representing graphically using Kivy.
If possible, I would like to write it so that when the root object is changed, the widgets in Kivy reflect the change. So far what I've noticed is that I can call on the object from the KV language when the widgets are initially created, and I can have those widgets modify the root object, but
widgets that should be "bound" to the root object variables do not get updated.
I'm looking to find a way to "bind" them so that I don't have to write a "refresh/reload" function that loops through my data object each time a change is made.
Below is sample code that shows how I have my code set up so far.
From what I understood in the kivy api docs, using ObjectProperty(object, rebind=True) should be doing what I am trying to have done.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty
class DataObject():
def __init__(self, name="foo", data="bar"):
self.name = name
self.data = data
class MainWindow(Screen):
pass
class WindowManager(ScreenManager):
data = DataObject()
data_obj = ObjectProperty(data, rebind=True)
kv = Builder.load_file("my.kv")
class MyMainApp(App):
def build(self):
return kv
if __name__ == '__main__':
MyMainApp().run()
WindowManager:
MainWindow:
<MainWindow>:
name: "Main"
GridLayout:
cols: 1
GridLayout:
cols: 2
Label:
text: root.manager.data_obj.name
Button:
text: "Change Name"
on_release:
root.manager.data_obj.name = "Bar"
print(root.manager.data_obj.name)
Pressing the Button "Change Name" changes the object data_obj.name from "foo" to "bar" and prints that to the console confirming it changed
I would expect the Label text to also change to "bar"

rebind works for Properties, and not for class attributes so your logic fails. One possible solution is that DataObject is an EventDispatcher and name, data is ObjectProperty:
from kivy.event import EventDispatcher
class DataObject(EventDispatcher):
name = ObjectProperty("foo")
data = ObjectProperty("bar")

Related

Insert a piece of code in the text of a Label that involves variables inside kv lang

I need to insert a piece of code in the text of a Label that involves variables inside kv lang as shown below:
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang.builder import Builder
from kivy.core.window import Window
Window.size=(200,200)
Builder.load_string("""
<Screen>
BoxLayout:
orientation:'vertical'
size:root.width,root.height
Label:
id:label
text:'Nothing'
Button:
id:button
text:'Insert'
on_release: root.insert_text()
""")
class Screen(Widget):
def insert_text(self):
self.ids.label.text='Something' if button.text=='Insert' else 'Nothing' #Label should say "Something" if Button's text says 'Insert'
class App(App):
def build(self):
return Screen()
if __name__=='__main__':
App().run()
When running this code I get the error: NameError: name 'button' is not defined
How can I avoid this error and have the entered code work within kv lang?
I already tried putting self.ids.button.text instead of button.text and despite not getting the error, it doesn't work inside the application either.
Edit: Also tried insert button.text as a raw string but kv lang just ignored it.
If you want Label text to change without a Button press, then you probably want to use a Property. You can reference a Property in kv and any changes to that Property will trigger evaluation where that Property is used. So, here is a modified version of your code that uses a Property named follow_changes that controls whether the Label text follows the Button text:
from kivy.app import App
from kivy.properties import BooleanProperty
from kivy.uix.widget import Widget
from kivy.lang.builder import Builder
from kivy.core.window import Window
Window.size = (200, 200)
Builder.load_string("""
<Screen>
BoxLayout:
orientation:'vertical'
size:root.width,root.height
Label:
id:label
text: 'Something' if root.follow_changes and button.text == 'Insert' else 'Nothing' # use follow_changes Property
Button:
id:button
text:'Insert'
on_release:
root.follow_changes = True # turn on following_changes
Button:
text: "Change Button Text"
on_release:
button.text = 'Abba'
""")
class Screen(Widget):
follow_changes = BooleanProperty(False) # Property to control Label text
class App(App):
def build(self):
return Screen()
if __name__=='__main__':
App().run()
Since the error is name 'button' is not defined, you just need to define button. Like this:
def insert_text(self):
button = self.ids.button # define button
self.ids.label.text='Something' if button.text=='Insert' else 'Nothing' #Label should say "Something" if Button's text says 'Insert'

How to clear textinputs by button from another page in Kivy?

My problem is probably mainly because of lack of skills but i couldnt find any similar posts. So I have textinputs on mainscreen. I need to have button in secondscreen which clear these textinputs.
I couldnt figure out how to can i call the clear_inputs method and pass textinput as arguments. I think with this clear_inputs method i could empty those textfields, but how to bind it to that button in another page?
Py.
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.properties import StringProperty, BooleanProperty
class MainScreen(Screen):
pass
class SecondScreen(Screen):
def clear_inputs(self, text_inputs):
for text_input in text_inputs:
text_input.text = ''
class ScreenManagement(ScreenManager):
def changescreen(self, value):
try:
if value !='main':
self.current = value
except:
print('No Screen named'+ value)
class testiApp(App):
def build(self):
self.title = 'Hello'
testiApp().run()
KV.
ScreenManagement:
MainScreen:
name:'Main'
SecondScreen:
name:'Page2'
<MainScreen>:
name:'Main'
BoxLayout:
orientation:'vertical'
GridLayout:
cols:2
Label:
text:'testfield1'
TextInput:
id: textfield1
Label:
text:'testfield2'
TextInput:
id: textfield2
Button:
text:'Next Page'
on_release: app.root.current ='Page2'
<SecondScreen>:
name:'Page2'
Button:
text:'Clear textfields'
on_release:
The following enhancements (kv file & Python script) are required to clear the TextInput's text in another screen.
kv file
In order to access the TextInput widgets, add an id: container to the instantiated object, GridLayout:
Each screen has by default a property manager that gives you the instance of the ScreenManager used.
Bind the on_release event to method, clear_inputs() without any argument
Snippets - kv file
<MainScreen>:
name:'Main'
BoxLayout:
orientation:'vertical'
GridLayout:
id: container
...
Button:
text:'Next Page'
on_release: root.manager.current ='Page2'
<SecondScreen>:
name:'Page2'
Button:
text:'Clear textfields'
on_release: root.clear_inputs()
Py file
Add import statement, from kivy.uix.textinput import TextInput
Use ScreenManager's get_screen('Main') function to get the instantiated object, MainScreen
Use for loop to traverse the children of GridLayout: via ids.container
Use isinstance() function to check for TextInput widget
Snippets - Py file
from kivy.uix.textinput import TextInput
...
class SecondScreen(Screen):
def clear_inputs(self):
main = self.manager.get_screen('Main')
for child in reversed(main.ids.container.children):
if isinstance(child, TextInput):
child.text = ''
If I'm undestanding correctly, what you want to do is use a button in page X (Main?) to change the text in page Y (Page2?). I'm not an expert on Kivy, so there might be a better way, but here are a few thoughts:
1) I tried giving a class attribute parent to all screens, which turned out to be a bad idea because the name was already in used by Kivy. You could simply change it to parent_ or something and give it a go yourself. What you want is to pass the "parent" as a parameter to __init__ on creation:
class ScreenManagement(ScreenManager):
def __init__(self, children_, **kwargs):
# you might need to save the children too
self.children_ = children_
def add_child(self, child):
# maybe as dict
self.children_[child.name] = child
class SecondScreen(Screen):
def __init__(self, parent_, **kwargs):
super().__init__(**kwargs)
# maybe the screen manager or the main app?
self.parent_ = parent_
self.name_ = "Second"
....
def clear_inputs(self, text_inputs):
....
class MainScreen(Screen):
def __init__(self, parent_, **kwargs):
super().__init__(**kwargs)
# maybe the screen manager or the main app?
self.parent_ = parent_
# you may want to
....
# Get the appropriate screen from the parent
self.parent_.children_["Second"].clear_inputs(...)
2) I also saw another way from a youtube tutorial. Instead of running your app directly, assign it to a variable and reference that variable. This might still need tampering for advanced usecases:
# Use the global variable within your classes/methods
class Whatever:
def whatever2(self, params):
app.something()
....
app = testiApp()
app.run()

Button Text does not Update

>> BACKGROUND :
I want to update/change the text of a Button in the SecondScreen with a press of a Button in the MainScreen. Well I did some research and did what I want, and when I checked in the terminal the text did change. BUUT, the text shown on the SecondScreen did not.
>> THIS IS WHAT I DID :
((Mind you that I'm only using snippets of code for example, I'm going to post the whole code below.))
Button:
text:"PRESS TO CHANGE TEXT"
on_press:
root.funcself()
## on press it goes to it's root and do the "funcself" function in it
which is :
class MainScreen(Screen):
def funcself(self):
app.second.funcscreen()
## it re-directs to the SecondScreen and do the "funcscreen" function
which is :
class SecondScreen(Screen):
def funcscreen(self):
self.ids["button"].text = "SUPPOSED TO CHANGE TO THIS"
and then I checked if I did it successfully by doing print(self.ids["button"].text), and yes!
It did change, but when I navigated to the next screen, the text shown still didn't change.
Anyone mind helping and explaining?
FULL CODE :
python file :
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import ScreenManager, Screen
class MainScreen(Screen):
def funcself(self):
app.second.funcscreen()
class SecondScreen(Screen):
def funcscreen(self):
value = self.ids["button"]
self.ids["button"].text = "SUPPOSED TO CHANGE TO THIS"
kv = Builder.load_file("reproduce.kv")
class reproduce(App):
second = SecondScreen()
def build(self):
return kv
def change_screen(self, x):
scrnmngr = self.root.ids["sm"]
scrnmngr.current = x
def check(self):
print(self.second.ids["button"].text)
if __name__ == "__main__":
app = reproduce()
app.run()
kivy file :
<MainScreen>:
GridLayout:
rows:2
Label:
text: "PRESS TO GO TO THE NEXT PAGE"
GridLayout:
cols:2
Button:
text:"PRESS TO CHANGE TEXT"
on_press:
root.funcself()
Button:
text:">>>"
on_press:
app.change_screen("second")
root.manager.transition.direction = "left"
<SecondScreen>:
GridLayout:
rows:2
Label:
id:label
text: "PRESS TO CHECK AND RETURN TO PREV PAGE"
Button:
id:button
text:"TEXT BEFORE CHANGE"
on_press:
app.change_screen("first")
root.manager.transition.direction = "right"
app.check()
GridLayout:
cols: 1
ScreenManager:
id:sm
MainScreen:
id:main
name:"first"
SecondScreen:
id:second
name:"second"
Root Cause
It did not change because there are two instances of SecondScreen() i.e. one instantiated in the kv file and the other one instantiated in the App class, reproduce(). The view presented is created from the kv file and the second instance does not has a view associated to it.
Solution
There are two solutions to the problem, and remove second = SecondScreen() from the App class.
Kivy Screen ยป default property manager
Each screen has by default a property manager that gives you the
instance of the ScreenManager used.
Using get_screen()
class MainScreen(Screen):
def funcself(self):
self.manager.get_screen('second').funcscreen()
Using App.get_running_app() & ids
class MainScreen(Screen):
def funcself(self):
App.get_running_app().root.ids.second.funcscreen()
Example
In the following example, there are two solutions provided but one of it is commented off.
main.py
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
class MainScreen(Screen):
def funcself(self):
self.manager.get_screen('second').funcscreen()
# App.get_running_app().root.ids.second.funcscreen()
class SecondScreen(Screen):
def funcscreen(self):
value = self.ids["button"]
self.ids["button"].text = "SUPPOSED TO CHANGE TO THIS"
kv = Builder.load_file("reproduce.kv")
class reproduce(App):
def build(self):
return kv
def change_screen(self, x):
scrnmngr = self.root.ids["sm"]
scrnmngr.current = x
def check(self):
print(self.second.ids["button"].text)
if __name__ == "__main__":
reproduce().run()
Output
The second attribute you define in your app class, is a new instantiation of the screen, and not really the instance you got in your screenmanager, which you add in kv. This is why when you check, you see its changed, but not on the right instance. And again when you call app.second.func, from mainscreen, again its the wrong instance.
But your app always has a root. In your case its the gridlayout. And every screen has a manager. There are a couple of ways to acces it. But you can do like this.
In your mainscreen class in kv:
Button:
text:"PRESS TO CHANGE TEXT"
on_press:
root.manager.get_screen("second").ids["button"].text = "Something"
Here it gets the screenmanager, and uses its get_screen() method to get the screen named second, and then the id's of that kv rule.

Kivy Python - Button to increment variable, without using NumericProperty

I'm a complete novice to both python and kivy having learnt python from codeacademy about a week ago and kivy from youtube tutorials.
Could someone please explain to me why the code below does not result in a screen with a label displaying n, which is incremented by the button?
Python file
import kivy
from kivy.app import App
from kivy.uix.button import Button, Label
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty
class Example(BoxLayout):
n = 0
def n_plus(self):
self.n += 1
class ExampleApp(App):
def build(self):
return Example()
example = ExampleApp()
example.run()
KV file
<Example>:
BoxLayout:
Label:
text: str(root.n)
Button:
text: "+1"
on_press: root.n_plus()
Then could you explain why making n = NumericProperty(0) makes this work?
I'd like to run some functions on n which don't seem to work on numeric properties.
Because when you use NumericProperty()
As the official document said:
It produces events such that when an attribute of your object changes,
all properties that reference that attribute are automatically
updated.
So, in short, it creates a binding relationship between your UI(.kv) and attribute of its class(.py)
But, actually, you can modify the UI by yourself without the help from the kivy framework. I changed your example as the following:
Add an id attribute to your widget
Access the id attribute by using self.ids.your_id_in_kv_file
But it's obviously not good, since now you need to update your UIby yourself everytime you want to update your UI. But with that XXXXProperty from kivy, you just need to change the value of that attribute, you don't need to worry about the UI at all.
Another disadvantage of the solution is that, when you need to change the UI, you need to change tons of code if you modify them all by yourself...
Here is the example:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.lang.builder import Builder
from kivy.uix.boxlayout import BoxLayout
Builder.load_string("""
<Example>:
BoxLayout:
Label:
id: lbl
text: "0"
Button:
text: "+1"
on_press: root.n_plus()
""")
class Example(BoxLayout):
def n_plus(self):
value = self.ids.lbl.text
self.ids.lbl.text = str(int(value) + 1)
class ExampleApp(App):
def build(self):
return Example()
if __name__ == '__main__':
ExampleApp().run()

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