Search code examples
pythonkivykivy-languagekivymd

Using ScreenManager in Kivy (Python) with several .py screens


I've recently started using the Kivy framework to create an app with multiple screens that I would like to merge, so that, for example, pressing a button on the login screen opens another page. I've been trying to use the ScreenManager library for days but I can't get it to work, there are several tutorials on the internet but everyone uses different variations. Below I attach the code of the login page where, by pressing the "Log in" button, the second screen should open.

login.py:

from kivy.core.text import LabelBase
from kivy.lang import Builder
from kivy.core.window import Window
from kivymd.app import MDApp

Window.size = (350, 580)

kv = """
MDFloatLayout:
    md_bg_color: 0, 0, 0, 1
    Image:
        source: "img\\logo5.png"
        pos_hint: {"center_x": .5, "center_y": .85}
        size_hint: .18, .18
    MDFloatLayout:
        size_hint: .9, .07
        pos_hint: {"center_x": .5, "center_y": .68}
        canvas:
            Color:
                rgb: 250/255, 250/255, 250/255, 1
            RoundedRectangle:
                size: self.size
                pos:self.pos
                radius: [4]
        canvas.before:
            Color:
                rgb: 217/255, 217/255, 217/255, 1
            Line:
                width: 1.1
                rounded_rectangle: self.x, self.y, self.width, self.height, 4, 4, 4, 4, 100
        TextInput:
            hint_text: "Phone number, username or e-mail"
            size_hint: 1, None
            pos_hint: {"center_x": .5, "center_y": .5}
            height: self.minimum_height
            background_color: 1, 1, 1, 0
            font_size: "14sp"
            font_name: "MRoboto"
            hint_text_color: 170/255, 170/255, 170/255, 1
            padding: 13
            cursor_color: 0, 0, 0, 1
    MDFloatLayout:
        size_hint: .9, .07
        pos_hint: {"center_x": .5, "center_y": .59}
        canvas:
            Color:
                rgb: 250/255, 250/255, 250/255, 1
            RoundedRectangle:
                size: self.size
                pos:self.pos
                radius: [4]
        canvas.before:
            Color:
                rgb: 217/255, 217/255, 217/255, 1
            Line:
                width: 1.1
                rounded_rectangle: self.x, self.y, self.width, self.height, 4, 4, 4, 4, 100
        TextInput:
            hint_text: "Password"
            size_hint: 1, None
            pos_hint: {"center_x": .5, "center_y": .5}
            height: self.minimum_height
            background_color: 1, 1, 1, 0
            font_size: "14sp"
            font_name: "MRoboto"
            password: "true"
            hint_text_color: 170/255, 170/255, 170/255, 1
            padding: 13
            cursor_color: 0, 0, 0, 1
    Button:
        text: "Log in"
        color: 1, 1, 1, 1
        size_hint: .9, .07
        pos_hint: {"center_x": .5, "center_y": .43}
        background_color: 1, 1, 1, 0
        font_size: "13sp"
        font_name: "BRoboto"
        canvas.before:
            Color:
                rgb: 98/255, 170/255, 243/255, 1
            RoundedRectangle:
                size: self.size
                pos: self.pos
                radius: [4]
    MDLabel:
        text: "Don't have an account?"
        color: 172/255, 172/255, 172/255, 1
        pos_hint: {"center_x": .74, "center_y": .095}
        font_size: "13sp"
        font_name: "MRoboto"
    MDTextButton:
        text: "Sign up"
        color: 98/255, 170/255, 243/255, 1
        pos_hint: {"center_x": .685, "center_y": .095}
        font_size: "13sp"
        font_name: "MRoboto"
    MDCheckbox:
        size_hint: None, None
        size: "48dp", "48dp"
        pos_hint: {"center_x": .1, "center_x": .1}
        on_active: app.show_password(*args)
    MDLabel:
        id: password_text
        text: "Show Password"
        pos_hint: {"center_x": .7, "center_x": .43}
"""
class Login(MDApp):

    def build(self):
        return Builder.load_string(kv)

    def show_password(self, checkbox, value):
        if value:
            self.root.ids.password.password = False
            self.root.ids.password_text.text = "Hide Password"
        else:
            self.root.ids.password.password = True
            self.root.ids.password_text.text = "Show Password"

if __name__ == "__main__":
    LabelBase.register(name="BRoboto", fn_regular="font\\Roboto-Bold.ttf")
    LabelBase.register(name="MRoboto", fn_regular="font\\Roboto-Medium.ttf")
    Login().run()

list.py:

from kivy.core.window import Window
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.uix.image import Image
from kivymd.app import MDApp
from kivymd.uix.list import IRightBodyTouch, ILeftBody
from kivymd.uix.selectioncontrol import MDCheckbox

Window.size = (350, 580)

kv = """
<ListItemWithCheckbox@OneLineAvatarIconListItem>:
    MyAvatar:
        source: "data/logo/kivy-icon-128.png"
    MyCheckbox:

<Lists@BoxLayout>
    name: "lists"
    orientation: "vertical"

    MDTopAppBar:
        title:"Hide the story to:"
        md_bg_color: app.theme_cls.primary_color
        elevation: 3

    ScrollView:

        MDList:
            id: scroll
"""

Builder.load_string(kv)

class MyCheckbox(IRightBodyTouch, MDCheckbox):
    pass

class MyAvatar(ILeftBody, Image):
    pass

class Users(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        self.title = "Liste"
        self.theme_cls.primary_palette = "Teal"
        self.theme_cls.theme_style = "Dark"
        list = Factory.Lists()
        for i in range(30):
            list.ids.scroll.add_widget(Factory.ListItemWithCheckbox(text="Item %d" % i))
        self.root = list

if __name__ == "__main__":
    Users().run() 

In my tests I tried to instantiate the ScreenManager both in the login.py code and in a third new file created from scratch. Thanks in advance.


Solution

  • I updated your code, so everything is in login.py. It is using the screen manager to switch between screens. You could also put the list class in a list.py file and move the imports it uses to that file.

    from kivy.lang import Builder
    from kivy.core.window import Window
    from kivymd.app import MDApp
    from kivymd.uix.screen import MDScreen
    from kivymd.uix.screenmanager import MDScreenManager
    from kivy.factory import Factory
    from kivy.uix.image import Image
    from kivymd import app
    from kivymd.uix.list import IRightBodyTouch, ILeftBody
    from kivymd.uix.selectioncontrol import MDCheckbox
    
    Window.size = (350, 580)
    
    kv = """
    WindowManager:
        id: manager
        LoginScreen:
            id: login_screen
        ListScreen:
            id: list_screen
    
    <LoginScreen>:
        name: 'login_screen'
        MDFloatLayout:
            md_bg_color: 0, 0, 0, 1
            Image:
                source: "img_logo5.png"
                pos_hint: {"center_x": .5, "center_y": .85}
                size_hint: .18, .18
            MDFloatLayout:
                size_hint: .9, .07
                pos_hint: {"center_x": .5, "center_y": .68}
                canvas:
                    Color:
                        rgb: 250/255, 250/255, 250/255, 1
                    RoundedRectangle:
                        size: self.size
                        pos:self.pos
                        radius: [4]
                canvas.before:
                    Color:
                        rgb: 217/255, 217/255, 217/255, 1
                    Line:
                        width: 1.1
                        rounded_rectangle: self.x, self.y, self.width, self.height, 4, 4, 4, 4, 100
                TextInput:
                    hint_text: "Phone number, username or e-mail"
                    size_hint: 1, None
                    pos_hint: {"center_x": .5, "center_y": .5}
                    height: self.minimum_height
                    background_color: 1, 1, 1, 0
                    font_size: "14sp"
                    # font_name: "MRoboto"
                    hint_text_color: 170/255, 170/255, 170/255, 1
                    padding: 13
                    cursor_color: 0, 0, 0, 1
            MDFloatLayout:
                size_hint: .9, .07
                pos_hint: {"center_x": .5, "center_y": .59}
                canvas:
                    Color:
                        rgb: 250/255, 250/255, 250/255, 1
                    RoundedRectangle:
                        size: self.size
                        pos:self.pos
                        radius: [4]
                canvas.before:
                    Color:
                        rgb: 217/255, 217/255, 217/255, 1
                    Line:
                        width: 1.1
                        rounded_rectangle: self.x, self.y, self.width, self.height, 4, 4, 4, 4, 100
                TextInput:
                    hint_text: "Password"
                    size_hint: 1, None
                    pos_hint: {"center_x": .5, "center_y": .5}
                    height: self.minimum_height
                    background_color: 1, 1, 1, 0
                    font_size: "14sp"
                    # font_name: "MRoboto"
                    password: "true"
                    hint_text_color: 170/255, 170/255, 170/255, 1
                    padding: 13
                    cursor_color: 0, 0, 0, 1
            Button:
                text: "Log in"
                color: 1, 1, 1, 1
                size_hint: .9, .07
                pos_hint: {"center_x": .5, "center_y": .43}
                background_color: 1, 1, 1, 0
                font_size: "13sp"
                # font_name: "BRoboto"
                canvas.before:
                    Color:
                        rgb: 98/255, 170/255, 243/255, 1
                    RoundedRectangle:
                        size: self.size
                        pos: self.pos
                        radius: [4]
                on_release: root.manager.current = 'list_screen'
            MDLabel:
                text: "Don't have an account?"
                color: 172/255, 172/255, 172/255, 1
                pos_hint: {"center_x": .74, "center_y": .095}
                font_size: "13sp"
                # font_name: "MRoboto"
            MDTextButton:
                text: "Sign up"
                color: 98/255, 170/255, 243/255, 1
                pos_hint: {"center_x": .685, "center_y": .095}
                font_size: "13sp"
                # font_name: "MRoboto"
            MDCheckbox:
                size_hint: None, None
                size: "48dp", "48dp"
                pos_hint: {"center_x": .1, "center_x": .1}
                on_active: app.show_password(*args)
            MDLabel:
                id: password_text
                text: "Show Password"
                pos_hint: {"center_x": .7, "center_x": .43}
                
    <ListItemWithCheckbox@OneLineAvatarIconListItem>:
        MyAvatar:
            source: "data/logo/kivy-icon-128.png"
        MyCheckbox:
        
    <ListScreen>
        name: 'list_screen'
        BoxLayout
            name: "lists"
            orientation: "vertical"
        
            MDTopAppBar:
                title:"Hide the story to:"
                md_bg_color: app.theme_cls.primary_color
                elevation: 3
        
            ScrollView:
        
                MDList:
                    id: scroll
    """
    
    
    class WindowManager(MDScreenManager):
        """ Window Manager """
        pass
    
    
    class ListItemWithCheckbox:
        pass
    
    
    class LoginScreen(MDScreen):
        pass
    
    
    class Login(MDApp):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.theme_cls.primary_palette = "Teal"
            self.theme_cls.theme_style = "Dark"
    
        def build(self):
            return Builder.load_string(kv)
    
        def show_password(self, checkbox, value):
            if value:
                self.root.ids.password.password = False
                self.root.ids.password_text.text = "Hide Password"
            else:
                self.root.ids.password.password = True
                self.root.ids.password_text.text = "Show Password"
    
    
    class MyCheckbox(IRightBodyTouch, MDCheckbox):
        pass
    
    
    class MyAvatar(ILeftBody, Image):
        pass
    
    
    class ListItemWithCheckbox:
        pass
    
    
    class ListScreen(MDScreen):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
    
        def on_enter(self):
            app.title = "Liste"
            # my_list = Factory.Lists()
            for i in range(30):
                self.ids.scroll.add_widget(Factory.ListItemWithCheckbox(text="Item %d" % i))
            # self.root = list
    
    
    if __name__ == '__main__':
        Login().run()
    

    First I create the WindowManager using the kv language.

    WindowManager:
        id: manager
        LoginScreen:
            id: login_screen
        ListScreen:
            id: list_screen
    

    Then I create each screen uning the format : followed by the name: 'screen_name' so I can reference it later.

    <LoginScreen>:
        name: 'login_screen'
        MDFloatLayout:
            md_bg_color: 0, 0, 0, 1
            Image:
        ...
    <ListScreen>
        name: 'list_screen'
        BoxLayout
        ...
    

    In the Button definition in the kv language I use:

    on_release: root.manager.current = 'list_screen'
    

    The root part tells it to look in the current screen for the manager and set the current screen to 'list_screen'

    The ListItemWithCheckbox@OneLineAvatarIconListItem: in the kv language, tell it to create a custom widget based on the OneLineAvatarIconListItem widget and set some options every time it is used.

    In the Python part, I define the matching custom widgets and the matching WindowManager class.

    class WindowManager(MDScreenManager):
        """ Window Manager """
        pass
    
    class ListItemWithCheckbox:
        pass
    
    class MyCheckbox(IRightBodyTouch, MDCheckbox):
        pass
    
    class MyAvatar(ILeftBody, Image):
        pass
    
    class ListItemWithCheckbox:
        pass
    
    class LoginScreen(MDScreen):
        pass
    

    Then I define the 2 screens.

    class LoginScreen(MDScreen):
        pass
    
    class ListScreen(MDScreen):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
    
        def on_enter(self):
            app.title = "Liste"
            # my_list = Factory.Lists()
            for i in range(30):
               self.ids.scroll.add_widget(Factory.ListItemWithCheckbox(text="Item %d" % i))
    

    The LoginScreen does not need any code in it so I just pass. The ListScreen uses the on_enter method which is run after the screen is fully drawn to set the window title and draw the checkboxes.

    Hopefully I got all the explanations correct.