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
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))
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.
I'm writing a simple app in KivyMD. According to the kivy documentation my .kv structure with MDNavigationDrawer and MDToolbar is right and everything works fine as long as the screens are empty. When I add content to them, the content instead of being under the Toolbar is above it. How can I fix it?
Here is my code:
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
KV = '''
<ContentNavigationDrawer>:
ScrollView:
MDList:
OneLineListItem:
text: "Screen 1"
on_press:
root.nav_drawer.set_state("close")
root.screen_manager.current = "scr 1"
OneLineListItem:
text: "Screen 2"
on_press:
root.nav_drawer.set_state("close")
root.screen_manager.current = "scr 2"
Screen:
MDToolbar:
id: toolbar
pos_hint: {"top": 1}
elevation: 10
title: "Test"
left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]]
NavigationLayout:
x: toolbar.height
ScreenManager:
Screen:
name: "scr 1"
ScrollView:
BoxLayout:
orientation: "vertical"
Button:
text: "Hello"
Button:
text: "I wish"
Button:
text: "I could"
Button:
text: "Finally get"
Button:
text: "This to work"
Screen:
name: "scr 2"
MDLabel:
text: "Screen 2"
halign: "center"
MDNavigationDrawer:
id: nav_drawer
ContentNavigationDrawer:
screen_manager: screen_manager
nav_drawer: nav_drawer
'''
class ContentNavigationDrawer(BoxLayout):
screen_manager = ObjectProperty()
nav_drawer = ObjectProperty()
class TestNavigationDrawer(MDApp):
def build(self):
return Builder.load_string(KV)
TestNavigationDrawer().run()
Output of the following code:
The Screen class is a RelativeLayout, so you must position its children as you would any RelativeLayout. Every child of the Screen gets the default size_hint of (1,1) and the default pos of (0,0), so you must adjust it if that is not what you want. In your case, the NavigationLayout will completely cover the Screen based on those default values. You can fix that by just adding a size_hint_y, like this:
NavigationLayout:
x: toolbar.height
size_hint_y: 1.0 - toolbar.height/root.height
This sets the size of the NavigationLayout so that it just meets the bottom of the MDToolbar instead of overlpping it.
It's the order of appearance that is the problem:
If the screen has other widgets such as cards and Layouts, all of these must appear first.
i.e. put the Navigation Layout and the respective screen manager at the bottom.
What comes last appears on top of everything else.
That worked for me.
A most elegant solution is to insert the MDToolbar and NavigationLayout into a BoxLayout with orientation: "vertical" so the NavigationLayout start at the bottom of the Toolbar, without calculating the relative position.
Screen:
BoxLayout:
orientation: "vertical"
MDToolbar:
MDNavigationLayout:
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()