Python Kivy - Call methods across different class - python

I am new to coding, a few months with Python and trying to wrap my head around Kivy. I think there is a simple solution to this but I am struggling when it comes to calling a method in one class from another. Not even sure if this is possible, my OOP wouldn't be very strong!!
Would appreciate if someone could explain this to me! I've looked online but still struggling to understand what I need to do.
i have a simple code that had a label and 3 toggle buttons, the label text changes to show how many toggle buttons are pressed. Below is the original code.
What I am trying to do create the toggle buttons using a loop so that the number of toggle buttons can be easily altered. i have achieved this but when I try and bind the method to the toggle button the code fails with. I also tried defining a method within the "Tbtn" class to call the Main.Counter() but this didn't work either.
The line
self.bind(on_state = Main.counter())
in the init of the toggle button is where i am going wrong I think.
Any help and even an explanation would be great. Not the first time I have been stuck on this!! Thanks
Original Code:
from kivy.app import App
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty
class Tbtn(ToggleButton):
pass
class Header_Box(BoxLayout):
pass
class Counter(BoxLayout):
pass
class Main(BoxLayout):
count = NumericProperty()
def counter(self,widget):
toggles = []
for child in self.ids.Seat_Box.children:
if isinstance(child, ToggleButton):
if child.state == 'down':
toggles.append(child.text)
self.count = len(toggles)
print(self.count)
class TestApp(App):
def build(self):
return Main()
TestApp().run()
The KV file:
<Main>:
name: "main"
BoxLayout:
orientation: "vertical"
Header_Box:
Label:
text: str(root.count)
Counter:
id: Seat_Box
Tbtn:
id: btn1
on_state: root.counter(self)
Tbtn:
id: btn2
on_state: root.counter(self)
Tbtn:
id: btn2
on_state: root.counter(self)
Code with for Loop:
from kivy.app import App
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty
class Tbtn(ToggleButton):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(on_state = Main().counter())
class Header_Box(BoxLayout):
pass
class Counter(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
for x in range(3):
btn = Tbtn()
self.add_widget(btn)
class Main(BoxLayout):
count = NumericProperty()
def counter(self,widget):
toggles = []
for child in self.ids.Seat_Box.children:
if isinstance(child, ToggleButton):
if child.state == 'down':
toggles.append(child.text)
self.count = len(toggles)
print(self.count)
class TestApp(App):
def build(self):
return Main()
TestApp().run()
KV file:
<Main>:
name: "main"
BoxLayout:
orientation: "vertical"
Header_Box:
Label:
text: str(root.count)
Counter:
id: Seat_Box

Firstly, Remove self.bind(on_state = Main().counter()).
I suggest you to solve this in .kv side.
Way 1-.kv side:
Add this below your .kv file:
<Tbtn>:
on_state: app.get_running_app().root.counter(self)
Way 2-.py side: Add this in Tbtn class.
def on_release(self):
App.get_running_app().root.counter(self)

Although the other answer already solved your issue, the following made me post this one.
Any help and even an explanation would be great...
Technically the following line,
self.bind(on_state = Main().counter())
is wrong for various reasons. Let's try to figure this out.
The method on_state is kind of generic one not a default event (like on_press etc.). That's why bind(on_state = some_callback) won't work.
Again you did Main().counter() which actually creates a new instance of Main (which may or may not be related to the root, and here it's of course not) and assigned to its method.
It seems you want to just access one of Main widget's (which happens to be the root widget here) method.
Since you used kvlang, this could be done more efficiently as follows,
<Tbtn>:
on_state: app.root.counter()
You can find more about this in the kvlang doc.
Now in .py you just define the class along with some other changes,
class Tbtn(ToggleButton):
pass
.
.
.
class Main(BoxLayout):
count = NumericProperty()
def counter(self): # Pass no extra args as you haven't done in 'on_state' method.
toggles = []
.
.
.

Related

Updating Kivy Widgets when Root Object Changes

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")

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.

Why isn't kv binding of the screen change working?

I've defined two buttons: one in kv and one in Python. They are located in different screens and are used to navigate between them. What I found strange is that the button that was defined in Python successfully switched the screen, while the one defined in kv did not. Perhaps I'm not accessing the App class method properly?
Here is the code of the issue:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.button import Button
Builder.load_string('''
<MyScreen1>:
Button:
id: my_bt
text: "back"
on_release: app.back
''')
class MyScreen1(Screen):
pass
class TestApp(App):
def here(self, btn):
self.sm.current = "back"
def back(self, btn):
self.sm.current = "here"
def build(self):
self.sm = ScreenManager()
s1 = Screen(name = "here")
bt = Button(text = "here",
on_release = self.here)
s2 = MyScreen1(name = "back")
#s2.ids['my_bt'].bind(on_release = self.back)
self.sm.add_widget(s1)
s1.add_widget(bt)
self.sm.add_widget(s2)
return self.sm
TestApp().run()
So if I define the switching function in kv (on_release), I can't go to the "here" screen. But if I uncomment that line in Python and comment the on_release: app.back instead, everything works fine.
I'm pretty sure that this is the correct way to access the current app, since it doesn't give me any errors (which means that the method was successfully located)
That's a subtle difference between kv and python: In kv you actually have to write the callback as a function call (a python expression), in this case:
on_release: app.back(self)

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