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.
Related
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.
im really new in Kivy and i would like to make an app where i could create a MDIconButton that is draggable, and if possible, droppable in any BoxLayout? Is that possible in KivyMD or Kivy? Also is there a Kivy function where whenever I hold down a button, it'll display some kind of small dialogue box that contains details that can be entered by the user. thanks!
Rather than use Drag-N-Drop from kivy-garden just use the DragBehavior class! It comes directly installed with Kivy and it'll save you having to install kivy-garden.
https://kivy.org/doc/stable/api-kivy.uix.behaviors.drag.html
Here is some example code of how it's used:
from kivy.uix.label import Label
from kivy.app import App
from kivy.uix.behaviors import DragBehavior
from kivy.lang import Builder
# You could also put the following in your kv file...
kv = '''
<DragLabel>:
# Define the properties for the DragLabel
drag_rectangle: self.x, self.y, self.width, self.height
drag_timeout: 10000000
drag_distance: 0
FloatLayout:
# Define the root widget
DragLabel:
size_hint: 0.25, 0.2
text: 'Drag me'
'''
class DragLabel(DragBehavior, Label):
pass
class TestApp(App):
def build(self):
return Builder.load_string(kv)
TestApp().run()
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.
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.
I try to design a GUI to handle stepper motors via ROS, kivy and python. You can find a minimal version of the GUI below. Actually I want to use a ROS message to update a kivy text input field (read only). In the minimal example, pressing the button should transfer the data of the first input field over a local ROS node to the second input field. Actually it seems, that the Callback within the rospy.Subscriber() doesn't enter the Test class.
Thank You for any suggestions!
main.py
import kivy
kivy.require('1.7.2')
import rospy
from std_msgs.msg import String
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
class Test(BoxLayout):
text_is = 'text before button press'
def Pub(self):
publish = self.ids.test_text_pub.text
try:
ROSNode.new_text_is.publish(publish)
except rospy.ROSInterruptException: pass
class ROSNode(Widget):
def Callback(publish):
print(publish.data) #check if Callback has been called
test.text_is = publish.data
new_text_is = rospy.Publisher('new_text_is', String, queue_size=10)
rospy.Subscriber('new_text_is', String, Callback)
rospy.init_node('talker', anonymous=True)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
test.kv
#:kivy 1.0
<Test>:
BoxLayout:
orientation: 'vertical'
Button:
id: test_button
text: 'publish'
on_press: root.Pub()
TextInput:
id: test_text_pub
text: 'text after button press'
TextInput:
id: test_text_sub
text: root.text_is
If you want your text to automatically update, you need to use Kivy properties.
from kivy.properties import StringProperty
class Test(BoxLayout):
text_is = StringProperty('text before button press')
Kivy properties support data binding, so any updates to the property will propagate through to any bound widget. Binding happens automatically in kv, so when you do this:
text: root.text_is
..you're telling Kivy that when root.text_is is updated, update my text also.
So I found the solution:
I had to add an on_release event to the button.
on_release: test_text_sub.text = root.text_is