This code works well, I can initiate the application with a dynamique number of screens and update the menu list in consequence.
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.list import OneLineListItem
KV = '''
Screen:
MDToolbar:
id: toolbar
pos_hint: {"top": 1}
elevation: 10
title: "MDNavigationDrawer"
left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]]
NavigationLayout:
x: toolbar.height
ScreenManager:
id: screen_manager
MDNavigationDrawer:
id: nav_drawer
BoxLayout:
canvas:
Rectangle:
pos: self.pos
size: self.size
orientation: 'vertical'
padding: "3dp"
spacing: "8dp"
AnchorLayout:
anchor_x: "center"
size_hint:1, None
halign: "center"
height: img_baniere_avekia_top.height
Image:
id: img_baniere_avekia_top
size_hint: None, None
size: "150dp", "93.56dp"
source: "menu_image.png"
ScrollView:
MDList:
id: menu_list
'''
screen_kv = '''
Screen:
MDLabel:
text: "Screen 1"
halign: "center"
'''
def chose_screen(widget, nav_drawer, screen_manager, text):
nav_drawer.set_state("close")
screen_manager.current = text
def add_tab(nav_drawer, screen_manager, menu_list, screen, title):
screen.name = title
menu_element = OneLineListItem(text=title)
menu_list.add_widget(menu_element)
screen_manager.add_widget(screen)
menu_element.bind(on_press = lambda w:chose_screen(w, nav_drawer, screen_manager, title))
class App(MDApp):
def build(self):
print("build app")
self.screen = Builder.load_string(KV)
return self.screen
def on_start(self):
print("start app")
for title in ["general","info"]:
self.add_tab(Builder.load_string(screen_kv), title)
def add_tab(self, screen, title):
add_tab(self.screen.ids.nav_drawer,
self.screen.ids.screen_manager,
self.screen.ids.menu_list,
screen,
title)
if __name__ == "__main__":
app = App()
app.run()
PROBLEM: If I want to add new screens after the app was build I can't access app.screen element !!!
The app.add_tab() method doesn't work anymore !!
Why can't I save the reference of the app's screen ? How should I do to keep this reference ?
Here is an example of what I want to do but it fail (same code, I just changed the end, trying to add_tab after the creation).
if __name__ == "__main__":
app = App()
app.add_tab(Builder.load_string(screen_kv), "Contact Us")
app.run()
ERROR MESSAGE: "AttributeError: 'App' object has no attribute 'screen'"
The error message is pretty clear - your App has no attribute screen because you haven't set that attribute yet, which in turn is because you haven't run your build method yet. Don't try to modify the screen before it exists.
Related
I have a kivymd app that has a screen with a button on it. When you click this button an MDCard will appear on the screen. When you click on this new MDCard it will call a function that will print a message on the terminal. However, I am having trouble getting this MDCard to call the function. I am getting the error:
AttributeError: 'MDCard' object has no attribute 'option'
The MDCard is in a separate kv string from the main kv string. Essentially I have two kv strings. When you press the button, the second kv string will be added as a widget to the first kv string.
I figured it is because the second kv string doesn't have a class as a root but I don't know how to do this. How can I get the MDCard to call the function??
MAIN.PY
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from button_screen import button_screen
MainNav = '''
<ContentNavigationDrawer>:
ScrollView:
MDList:
OneLineListItem:
text: 'Go to Button Screen'
on_press:
root.nav_drawer.set_state("close")
root.screen_manager.current = "go_to_button_screen"
Screen:
MDToolbar:
id: toolbar
pos_hint: {"top": 1}
elevation: 10
left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]]
MDNavigationLayout:
x: toolbar.height
ScreenManager:
id: screen_manager
Screen:
name: "words_nav_item"
button_screen:
name: "go_to_button_screen"
MDNavigationDrawer:
id: nav_drawer
ContentNavigationDrawer:
screen_manager: screen_manager
nav_drawer: nav_drawer
'''
class ContentNavigationDrawer(BoxLayout):
screen_manager = ObjectProperty()
nav_drawer = ObjectProperty()
class main_test(MDApp):
def build(self):
self.theme_cls.primary_palette = "Red"
return Builder.load_string(MainNav)
main_test().run()
BUTTON SCREEN .PY FILE
from kivy.lang import Builder
from kivymd.uix.screen import MDScreen
button_screen_kv = '''
<button_screen>:
MDGridLayout:
cols: 1
size: root.width, root.height
spacing: 40
md_bg_color: [0,0,.1,.1]
MDGridLayout:
cols: 1
size_hint_y: None
height: 40
MDGridLayout:
cols: 1
Button:
text: "Click to add card"
on_release:
root.add_card("card 1")
MDGridLayout:
id: add_card_here_id
cols: 1
'''
md_card_kv = '''
MDCard:
orientation: 'vertical'
size_hint: None, None
size: "360dp", "120dp"
ripple_behavior: True
on_release:
root.option("MDCard was clicked")
MDLabel:
id: LabelTextID
text: "this is an MDCard"
halign: 'center'
'''
class button_screen(MDScreen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Builder.load_string(button_screen_kv)
self.md_card_widget = Builder.load_string(md_card_kv)
def option(self, string):
print(f"{string}")
def add_card(self, *args):
self.ids.add_card_here_id.add_widget(self.md_card_widget)
I came up with the following that works however, I am still new to programming so I don't know if this is a stable solution. Please advise me there
The Solution:
I added another class class user_card(MDGridLayout) in the buttonscreen.py and placed the kv string there.
I set that class as the root widget in the kv string
in the init method I placed a function that will return the kv string return self.user_card_string
then I added that class as a parameter in the add_widget and passed the col as an argument: self.ids.add_card_here_id.add_widget(user_card(cols=1))
Everything works but I have never used two classes before. So I am unsure if this will present a future problem.
button_screen.py:
from kivy.lang import Builder
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.screen import MDScreen
button_screen_kv = '''
<button_screen>:
MDGridLayout:
cols: 1
size: root.width, root.height
spacing: 40
md_bg_color: [0,0,.1,.1]
MDGridLayout:
cols: 1
size_hint_y: None
height: 40
MDGridLayout:
cols: 1
Button:
text: "Click to add card"
on_release:
root.add_card("card 1")
MDGridLayout:
id: add_card_here_id
cols: 1
'''
md_card_kv = '''
<user_card>:
MDGridLayout:
cols: 1
MDCard:
orientation: 'vertical'
size_hint: None, None
size: "360dp", "120dp"
ripple_behavior: True
on_release:
root.option()
MDLabel:
id: LabelTextID
text: "this is an MDCard"
halign: 'center'
'''
class user_card(MDGridLayout):
user_card_string = Builder.load_string(md_card_kv)
def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
self.load_card()
def option(self, *args):
print("MDCard was clicked")
def load_card(self):
return self.user_card_string
class button_screen(MDScreen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Builder.load_string(button_screen_kv)
def add_card(self, *args):
self.ids.add_card_here_id.add_widget(user_card(cols=1))
Problem
I have a screen (OrderScreen) that populates with buttons if there is data to be processed. I would like the user to click one of the buttons to be brought to another screen (MenuScreen) to process the data. While my intention is to populate the next screen with data from the button, I am currently just trying to get the ScreenManager to change to the next screen after a button press. I added a pass_data() method to the OrderButton and tried to trigger the screen manager there but self.manager.current and root.manager.current were throwing exceptions. Any help would be greatly appreciated.
recycleview_test.py
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.recycleview import RecycleView
from kivy.uix.screenmanager import ScreenManager, Screen
from random import randint
class MenuScreen(Screen):
def quit(self):
App.get_running_app.stop()
Window.close()
class OrderScreen(Screen):
pass
class OrderButton(Button):
def __init__(self, **kwargs):
super(OrderButton, self).__init__(**kwargs)
def pass_data(self):
print("button pushed")
class OrderScroll(RecycleView):
def __init__(self, **kwargs):
super(OrderScroll, self).__init__(**kwargs)
self.data = [{'text': str(f"Make {randint(10, 25)} items from package #{randint(1,4)}")} for x in range(12)]
class WindowManager(ScreenManager):
pass
class RecycleApp(App):
def build(self):
return WindowManager()
if __name__ == "__main__":
RecycleApp().run()
recycle.kv
#:import Factory kivy.factory.Factory
#: import ScreenManager kivy.uix.screenmanager.ScreenManager
#: import Screen kivy.uix.screenmanager.ScreenManager
#:import App kivy.app.App
<OrderScroll>:
viewclass: 'OrderButton'
manager: None
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
padding: 20
spacing: 10
<OrderButton>:
manager: None
font_size: 32
bold: True
on_release:
root.pass_data()
<WindowManager>:
id: screen_manager
OrderScreen:
id: order_screen
name: "OrderScreen"
manager: screen_manager
MenuScreen:
id: menu_screen
name: 'MenuScreen'
manager: screen_manager
<OrderScreen>:
BoxLayout:
orientation: "vertical"
Label:
text: "Data Buttons"
font_size: 64
size_hint_y: None
# pos_hint: {"x":0, "y":1}
height: 200
OrderScroll:
<MenuScreen>:
BoxLayout:
orientation: "vertical"
Label:
text: "Made it"
font_size: 64
FloatLayout:
Button:
text: 'Keep going'
font_size: 48
size_hint: .8,.5
pos_hint: {"center_x": .5, "center_y": .1}
FloatLayout:
Button:
text: 'Quit'
size_hint: .15,.3
pos_hint: {"center_x": .5, "center_y": .5}
on_release:
root.quit()
Try to create an object of screen manager:
class RecycleApp(App):
def build(self):
self.sm = WindowManager()
return self.sm
And then access it by:
App.get_running_app().sm.current = 'MenuScreen' # in Python
app.sm.current = 'MenuScreen' # in kv
I want to open the kivy settings inside a screen from the kivymd navigationdrawer. The default settings only open in a new window that completely ignore the color theme of the app. Any suggestion on how to make the settings a child of the screen manager?
Here is the guiApp.py:
from kivy.app import App
import kivymd
from kivymd.theming import ThemeManager
from kivy.uix.settings import Settings, SettingsWithSidebar
class guiApp(App):
theme_cls = ThemeManager()
theme_cls.primary_palette = 'BlueGray'
theme_cls.theme_style = 'Light'
def build(self):
self.settings_cls = SettingsWithSidebar
guiApp().run()
and the gui.kv:
NavigationLayout:
MDNavigationDrawer:
NavigationDrawerSubheader:
text: 'Operation Menu'
NavigationDrawerIconButton:
icon: 'information-outline'
text: 'Introduction'
on_release: screen_manager.current = 'screen_info'
NavigationDrawerIconButton:
icon: 'settings'
text: 'Settings'
on_release: screen_manager.current = 'screen_settings'
on_release: app.open_settings()
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: 'My GUI'
md_bg_color: app.theme_cls.primary_color
left_action_items: [['menu', lambda x: root.toggle_nav_drawer()]]
ScreenManager:
id: screen_manager
Screen:
name: 'screen_info'
MDLabel:
text: 'This page will be used for information on how to use the App '
theme_text_color : 'Hint'
valign: 'middle'
halign: 'center'
Screen:
name: 'screen_settings'
BoxLayout:
Instead of using app.open_settings(), you can use app.create_settings(), to get the setting widget, that you can directly attach to a Screen.
Add the on_start method to guiApp class
[...]
class guiApp(App):
[...]
def on_start(self):
s = self.create_settings()
self.root.ids.settings_content.add_widget(s)
And give an id to the BoxLayout of your screen_settings
[...]
Screen:
name: 'screen_settings'
BoxLayout:
id: settings_content
I'm looking for a way to change a part of a screen between 'DownPanel1' and 'DownPanel1' but I would like to avoide creating nex screen class 'ToAvoid'. Is it possible?
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
kv = '''
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
ScreenManagement:
transition: FadeTransition()
SomeScreen:
ToAvoid:
<Menu#RelativeLayout>
id: main_menu
size_hint_x: None
width: 120
Button:
size_hint_y: None
pos: root.x, root.top - self.height
text: 'SomeScreen'
on_press: app.root.current = "SomeScreen"
<UpScreen>:
BoxLayout:
Button:
text: 'switch'
on_press: app.root.current = "ToAvoid"
<DownPanel1>:
Button:
text: 'DownPanel1'
#on_press:
<DownPanel2>:
Button:
text: 'DownPanel2'
#on_press:
<SomeScreen>:
name: 'SomeScreen'
BoxLayout:
orientation: 'horizontal'
Menu:
BoxLayout:
orientation: 'vertical'
UpScreen:
DownPanel1:
<ToAvoid>:
name: 'ToAvoid'
BoxLayout:
orientation: 'horizontal'
Menu:
BoxLayout:
orientation: 'vertical'
UpScreen:
DownPanel2:
'''
class DownPanel1(BoxLayout):
pass
class DownPanel2(BoxLayout):
pass
class UpScreen(Screen):
pass
class SomeScreen(Screen):
pass
class ToAvoid(Screen):
pass
class ScreenManagement(ScreenManager):
pass
sm = Builder.load_string(kv)
class TestApp(App):
def build(self):
return sm
if __name__ == '__main__':
TestApp().run()
How about some inception? Just put another ScreenManager inside of the other.
Try this example:
from kivy.app import App
from kivy.lang import Builder
KV = """
ScreenManager:
Screen:
name: "1st"
Button:
text: "next"
on_release:
root.current = "2nd"
Screen:
name: "2nd"
BoxLayout:
Button:
text: "3rd to avoid"
on_release:
root.current = "3rd"
ScreenManager:
id: sm2
Screen:
name: "inner1"
Button:
text: "go to inner 2"
on_release:
sm2.current = "inner2"
Screen:
name: "inner2"
Label:
text: "This is inner 2!"
Screen:
name: "3rd"
Label:
text: "To Avoid!"
"""
class MyApp(App):
def build(self):
return Builder.load_string(KV)
if __name__ == "__main__":
MyApp().run()
I am trying to do a very simple example in kivy (kv) as follows:
#:import Toolbar kivymd.toolbar.Toolbar
BoxLayout:
orientation: 'vertical'
Toolbar:
id: toolbar
title: 'My Toolbar'
md_bg_color: app.theme_cls.primary_color
background_palette: 'Primary'
background_hue: '500'
left_action_items: [['arrow-left', app.root.ids.scr_mngr.current = 'screen1' ]]
right_action_items: [['arrow-right', app.root.ids.scr_mngr.current = 'screen2' ]]
ScreenManager:
id: scr_mngr
Screen:
name: 'screen1'
Toolbar:
title: "Screen 1"
Screen:
name: 'screen2'
Toolbar:
title: "Screen 2"
This will fail because both left_action_items and right_action_items expect a list of pairs: [name_of_icon, expression]. On the other hand, when you deal with buttons, the statement would be legal for instance if we do something like:
on_release:
app.root.ids.scr_mngr.current = 'screen1'
On the other hand, the right approach for left_action_item would be something like:
left_action_items: [['arrow-left', lambda x: app.root.ids.scr_mngr.current = 'screen1' ]]
But this is not legal, because you cannot perform such assigment in lambda under python.
What would be right approach for left_action_items to change the screen?
This is the simplest way I have found to change the screen clicking on a MDToolbar icon:
main.py
from kivymd.app import MDApp
from kivy.uix.screenmanager import ScreenManager, Screen, NoTransition
from kivy.lang import Builder
Builder.load_file("telas.kv")
class Tela1(Screen):
pass
class Tela2(Screen):
pass
class MainApp(MDApp):
def build(self):
self.sm = ScreenManager(transition=NoTransition())
self.sm.add_widget(Tela1(name="tela1"))
self.sm.add_widget(Tela2(name="tela2"))
return self.sm
def change_screen(self, tela):
self.sm.current = tela
if __name__ == "__main__":
MainApp().run()
telas.kv
<Tela1>:
BoxLayout:
orientation: "vertical"
MDToolbar:
title: "My App"
MDRoundFlatButton:
text: "Go to Tela 2"
on_release: app.root.current = "tela2"
MDLabel:
text: "Tela 1"
<Tela2>:
BoxLayout:
orientation: "vertical"
MDToolbar:
title: "Tela 2"
left_action_items: [["arrow-left", lambda x: app.change_screen("tela1")]]
MDLabel:
text: "Tela 2"
You could make your root class into a python class, and have a change_screen method there. Also make your screenmanager to an ObjectProperty in the root class.
Then use partial in kv to be able to pass arguments to the method.
Try like this:
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from kivymd.theming import ThemeManager
class MyLayout(BoxLayout):
scr_mngr = ObjectProperty(None)
def change_screen(self, screen, *args):
self.scr_mngr.current = screen
KV = """
#:import Toolbar kivymd.toolbar.Toolbar
#:import partial functools.partial
MyLayout:
scr_mngr: scr_mngr
orientation: 'vertical'
Toolbar:
id: toolbar
title: 'My Toolbar'
left_action_items: [['arrow-left', partial(root.change_screen, 'screen1') ]]
right_action_items: [['arrow-right', partial(root.change_screen, 'screen2') ]]
ScreenManager:
id: scr_mngr
Screen:
name: 'screen1'
Toolbar:
title: "Screen 1"
Screen:
name: 'screen2'
Toolbar:
title: "Screen 2"
"""
class MyApp(App):
theme_cls = ThemeManager()
def build(self):
return Builder.load_string(KV)
MyApp().run()
There are multiple options, you can do
left_action_items: [('arrow-left', (app.root.ids.scr_mngr, 'current', 'screen1'))]
and later do:
for icon, expr in self.left_action_items:
setattr(*expr)
If you really want an executable expression you can do:
left_action_items: [('arrow-left', lambda: setattr(app.root.ids.scr_mngr, 'current' 'screen1'))]
Also one more method. That I using:
left_action_items: [['arrow-left', lambda x: root.manager.change_screen("main_screen")]]
Realization of function change_screen() inside the screen manager:
def change_screen(self, screen):
# the same as in .kv: app.root.current = screen
self.current = screen