Kivy: Using Toolbar together with ScreenManager. What I'm doing wrong here? - python

So I have a Toolbar that work perfectly well on a single screen, but I need two screens. One main screen and one setup screen. This should be easy done with ScreenManager, I hoped.
When I run this code I get the following error: AttributeError: 'super' object has no attribute 'getattr' What I'm doing wrong?
from kivymd.app import MDApp
from kivy.lang.builder import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.uix.menu import MDDropdownMenu
from kivy.metrics import dp
kv = """
ScreenManager:
MainScreen:
SetupScreen:
<MainScreen>:
name: 'main'
MDToolbar:
id: tool1
title:'My Demo App'
pos_hint:{'top':1}
right_action_items : [["dots-vertical", lambda x: app.menu.open()]]
<SetupScreen>:
name: 'setup'
"""
class MainScreen(Screen):
pass
class SetupScreen(Screen):
pass
# Create the screen manager
sm = ScreenManager()
sm.add_widget(MainScreen(name='main'))
sm.add_widget(SetupScreen(name='setup'))
class DemoApp(MDApp):
def build(self):
screen = Builder.load_file('test.kv')
menu_items = [
{
"text": f"Option {opt}",
"viewclass": "OneLineListItem",
"height": dp(40),
"on_release": lambda x=f"Option {opt}": self.menu_callback(x),
} for opt in range(4)
]
menu = MDDropdownMenu(
caller=sm.ids.tool1,
items=menu_items,
width_mult=3
)
return screen
def menu_callback(self, text_item):
print(text_item)
DemoApp().run()

I keep seeing this same error in many posts. When your build() method returns the result from Builder.load_file() or Builder.load_string(), then your root widget (and your entire GUI) is defined in the kv. So the lines:
# Create the screen manager
sm = ScreenManager()
sm.add_widget(MainScreen(name='main'))
sm.add_widget(SetupScreen(name='setup'))
are creating another instance of your GUI, but that instance is not used, and any references to that sm will have no effect on the ScreenManager that is actually in your GUI (the one built via the kv). So, you can start by eliminating those lines completely.
Then, to fix the actual problem you need to change your construction of the MDDropdownMenu to something like:
self.menu = MDDropdownMenu(
# caller=sm.ids.tool1,
caller=screen.get_screen('main').ids.tool1,
items=menu_items,
width_mult=3
)
using self.menu instead of just menu saves a reference to the menu. Otherwise, the menu is created, then discarded. Since sm is not part of your GUI, you must use a reference to your actual GUI. The line:
caller=screen.get_screen('main').ids.tool1,
uses the ScreenMananger (screen) that is returned by the Builder. Then using get_screen(), it gets the main Screen (since that is the one that contains the tool1 id). And finally uses that id to get a reference to the MDToolbar.

Related

Black Screen When Using on_enter Calling a Function with Kivy/Python

I'm new to Python, and especially new to Kivy.
I'm sure that whatever I'm doing is a simple fix, but I just cannot figure it out for the life of me.
I've been doing this all in a Python file, with no my.kv.
What I'm trying to do is call a function upon entering the first screen of my app, but when I do this it just gives me a blank screen.
Sorry if my code is an absolute mess.
This is my code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
import requests
import json
Builder.load_string("""
<Manager>:
BuildScreen:
SubmitScreen:
<BuildScreen>:
name: 'page1'
on_enter: app.BuildAnswer()
<SubmitScreen>:
name: 'page2'
on enter: app.SubmitAnswer()
GridLayout:
cols:1
row_force_default:True
row_default_height:40
Button:
text:"Return"
on_release: root.manager.current = 'page1'
""")
class MainWidget(Widget):
pass
class Manager(ScreenManager):
pass
class BuildScreen(Screen):
pass
class SubmitScreen(Screen):
pass
class TheLabApp(App):
def __init__(self,**kwargs):
super(TheLabApp, self).__init__(**kwargs)
def BuildAnswer(self):
layout = GridLayout(cols=1, row_force_default=True, row_default_height=40)
self.spell = TextInput(hint_text = "Enter Spell", multiline=False)
button = Button(text="Get Spell", on_release=self.SubmitAnswer)
layout.add_widget(self.spell)
layout.add_widget(button)
return layout
def SubmitAnswer(self):
user_input = self.spell.text
#making input into url ready thingy
making_string = ''.join(str(x) for x in user_input)
x = '-'.join(making_string.split())
url = requests.get('https://www.dnd5eapi.co/api/spells/' + x)
#making it look pretty
pretty_spells = json.dumps(url.json(), indent=2)
#making it so I can get values from json
resp = json.loads(pretty_spells)
print(resp['name'])
print(resp['range'])
#the rest is just printing more of the spell's information
def build(self):
sm=ScreenManager()
sm.add_widget(BuildScreen(name="page1"))
sm.add_widget(SubmitScreen(name="page2"))
return sm
Any and all help would be incredibly appreciated, as I've been trying to find a solution for a couple days now.
A couple problems with your code.
First, the BuildAnswer() method creates and returns a layout, but returning something from an on_enter method has no effect. If you want that layout to appear in the BuildScreen, you must explicitly add that layout to the BuildScreen instance.
Second, defining the BuildAnswer() method in the TheLabApp class makes it difficult to access the BuildScreen instance. This is because the on_enter method is triggered very early in the process (before the root of the App is assigned).
I suggest moving the BuildAnswer() method to the BuildScreen class, and calling add_widget() to actually add the created layout to the BuildScreen instance. To do that, start by modifying your kv:
<BuildScreen>:
name: 'page1'
on_enter: self.BuildAnswer() # reflects new location of BuildAnswer()
And modifying your python code:
class BuildScreen(Screen):
def BuildAnswer(self):
layout = GridLayout(cols=1, row_force_default=True, row_default_height=40)
self.spell = TextInput(hint_text="Enter Spell", multiline=False)
button = Button(text="Get Spell", on_release=App.get_running_app().SubmitAnswer) # to access the SubmitAnswer method
layout.add_widget(self.spell)
layout.add_widget(button)
self.add_widget(layout) # add layout to the GUI
# return layout
And remove the BuildAnswer() method from the TheLabApp class.

Access Kivy settings values during initial build of App

I am learning how to implement the Kivy settings panel. It'd be perfect for several use cases, but I cannot figure out how to get the values of the settings to show in my app immediately after the build.
I borrowed this example code from PalimPalims answer here. It works great when you change the settings, but prior to changing the value in the settings panel, the Label widget has no text. I tried adding it in the kv language section text text: App.get_running_app().config.get('Label','content') after import App into the build section.
I also tried assigning the widgets value in the Apps build function but kept getting an error 'MyApp has no ids'. I have to believe this is doable and I'm just reading over the method in the docs.
from kivy.app import App
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.config import Config
class Labelwithconfig(Label):
def check_label(self):
self.text = App.get_running_app().config.get('Label','content')
kv_str = Builder.load_string("""
BoxLayout:
orientation: 'vertical'
Labelwithconfig:
id: labelconf
Button:
text: 'open settings'
on_press: app.open_settings()
""")
class MyApp(App):
def build_config(self, config):
config.setdefaults('Label', {'Content': "Default label text"})
def build_settings(self, settings):
settings.add_json_panel("StackOverflow Test Settings", self.config, data="""
[
{"type": "options",
"title": "Label text System",
"section": "Label",
"key": "Content",
"options": ["Default label text", "Other Label text"]
}
]"""
)
def on_config_change(self, config, section, key, value):
self.root.ids.labelconf.check_label()
def build(self):
return kv_str
if __name__ == '__main__':
MyApp().run()
text: App.get_running_app().config.get('Label','content') won't display your text when your app starts up because the content of your kv file is loaded before your App class has fully loaded. To do what you want, overwrite the on_start method of the App class (this is a super handy trick that is hard to discover sometimes for new users).
def on_start(self):
self.root.ids.labelconf.text = self.config.get('Label','content')
From the kivy docs:
on_start()
Event handler for the on_start event which is fired after
initialization (after build() has been called) but before the
application has started running.
Basically, you are able to access your app's variables like self.whatever once the build() function has finished. on_start() is automatically called when build() finishes.

Kivy | Can't set 2 buttons on_press binding in screen [duplicate]

I would like to know how to change screens using an on_press event binded to a button, without using a KV file/KV language.
I have read through the Kivy documentation, but have only been able to find solutions using a KV file.
Example:
on_press: root.manager.current = 'screen2'
I can also change the screen in the main python file using:
screenmanager.current = 'screen2'
But I cant figure out how to achieve the same using a button.
A working example with two screens, no kv file everything done in Python:
import kivy
kivy.require('1.8.0')
from kivy.app import App
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
class ScreenOne(Screen):
def __init__ (self,**kwargs):
super (ScreenOne, self).__init__(**kwargs)
my_box1 = BoxLayout(orientation='vertical')
my_label1 = Label(text="BlaBlaBla on screen 1", font_size='24dp')
my_button1 = Button(text="Go to screen 2",size_hint_y=None, size_y=100)
my_button1.bind(on_press=self.changer)
my_box1.add_widget(my_label1)
my_box1.add_widget(my_button1)
self.add_widget(my_box1)
def changer(self,*args):
self.manager.current = 'screen2'
class ScreenTwo(Screen):
def __init__(self,**kwargs):
super (ScreenTwo,self).__init__(**kwargs)
my_box1 = BoxLayout(orientation='vertical')
my_label1 = Label(text="BlaBlaBla on screen 2",font_size='24dp')
my_button1 = Button(text="Go to screen 1",size_hint_y=None, size_y=100)
my_button1.bind(on_press=self.changer)
my_box1.add_widget(my_label1)
my_box1.add_widget(my_button1)
self.add_widget(my_box1)
def changer(self,*args):
self.manager.current = 'screen1'
class TestApp(App):
def build(self):
my_screenmanager = ScreenManager()
screen1 = ScreenOne(name='screen1')
screen2 = ScreenTwo(name='screen2')
my_screenmanager.add_widget(screen1)
my_screenmanager.add_widget(screen2)
return my_screenmanager
if __name__ == '__main__':
TestApp().run()
One simple way to accomplish this is to define your own button subclass:
class ScreenButton(Button):
screenmanager = ObjectProperty()
def on_press(self, *args):
super(ScreenButton, self).on_press(*args)
self.screenmanager.current = 'whatever'
The on_press method is automatically called when the button is pressed, so the screenmanager's current property will be changed.
Then you can have code something like:
sm = ScreenManager()
sc1 = Screen(name='firstscreen')
sc1.add_widget(ScreenButton(screenmanager=sm))
sc2 = Screen(name='whatever')
sc2.add_widget(Label(text='another screen'))
sm.add_widget(sc1)
sm.add_widget(sc2)
Clicking the button should switch the screens as required.
Another way (which is probably how kv language actually does it) would be to manually use the bind method.
def switching_function(*args):
some_screen_manager.current = 'whatever'
some_button.bind(on_press=switching_function)
This would mean that switching_function is called whenever some_button is pressed. Of course there is a lot of flexibility here regarding how and when you define the function, so (for instance) you could do something more general like pass the screenmanager as the first argument to the function.
I didn't test this code and it isn't a complete app, but hopefully the meaning is clear. Either method should work fine, you can choose the way that seems most sensible. I might construct a more complete example later.
Another solution, was to use the setter method of EventDispatcher, to get a reference to the setter function for screen_manager.current
button.bind(on_press=partial(sm.setter('current'), (sm, 'whatever'))
of course, it's not very sexy, that's why kv is often a cleaner solution to these things, but it should work.
ps: in case you don't know about it, partial comes from the functools module, and it's often useful to build these kind of callbacks with a preloaded parameter.

Understanding Kivy properities and binding methods

I am having problems understanding the usage of custom Properities and ways of binding methods to events.
Here's my code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.properties import StringProperty
kivy_lang = '''
<MainWidget>:
on_my_property: my_label.text = 'from button bind method via StringProperty' + my_property
Label:
id: my_label
text: root.my_property
Button:
id: my_button
text: 'intro button'
'''
class MainWidget(BoxLayout):
# bind some properties
my_property = StringProperty('0')
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
# if needed to do sth on widget construction
self.ids.my_button.bind(on_press=self.my_method)
def my_method(self,*args,**kwargs):
self.my_property = str(int(self.my_property)+1)
self.ids.my_button.text = 'new'
class MyApp(App):
def build(self):
Builder.load_string(kivy_lang)
return MainWidget()
if __name__ == '__main__':
MyApp().run()
When I run it it renders OK, but when I click a button, as a result I get
NameError: name 'my_property' is not defined
I tried binding method for Button in kv lang with (and removing whole 'init()' on python side):
on_press: root.my_method
and then when I press button the app doesn't crash but nothing happens
Can someone explain me how to adjust this code to work?
I understand the code is a little 'mixed techniques' but I did it that way to get to know different approaches, so I would appreciate if You don't turn it all around :)
1/ you are missing 'self' before 'my_property' in 'on_my_property' bindind, hence the crash
2/ in kv bindings. the python code is called as written, so you need '()' after 'root.my_method', or the statement has no effect.

How do I set widget attributes by calling a function in Kivy Python?

Suppose I have a ThemeManager object as a class attribute in my RootWidget like so:
class RootWidget(Widget):
theme = ThemeManager()
The ThemeManager defines a function that returns a hex color.
class ThemeManager:
def get_color(self):
return '#ffffffff'
Let's say I create a Button in my RootWidget using a kv file. How would I be able to call the ThemeManager functions from the kv file? Here's an example that doesn't work:
import kivy
kivy.require('1.9.0')
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.lang import Builder
class ThemeManager:
def get_color(self):
return '#ffffffff'
class RootWidget(Widget):
theme = ThemeManager()
my_kv = Builder.load_string("""
#: import get_color_from_hex kivy.utils.get_color_from_hex
RootWidget:
Button:
color: get_color_from_hex(app.root.theme.get_color())
text: "Test"
""")
class TestApp(App):
def build(self):
return my_kv
if __name__ == '__main__':
TestApp().run()
Since your question is already answered, here's a stab at the explanation, it's actually pretty simple (I think).
app.root is None at the point where your Button is trying to read the function. Because the order of things is (loosely):-
RootWidget created
Once it and all it's children are done (init completed), the object gets passed to the line in build()
app.root is only set on the call to TestApp.run()
As to why 3. happens, the init method in app.py initializes self.root as None. It can then be set by load_kv (loads a kv with the same name as this app) or by run (which is what happens most of the time).
So you can call app.root in your on_press events (because these only happen in response to user interaction, when the app is fully created) but not in one-off widget initialization events.
Interestingly enough, root is not defined as an ObjectProperty in app.py, which means you can't bind to changes in it like you can with, say, the title and icon. Not sure if it'd ever change though, so this is probably moot.

Categories

Resources