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

Question:

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.

Asked By: Henry

||

Answers:

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.

Answered By: Givver

Let me show you Kivy screens basics. Below it’s an example app with 3 screens. Each screen contain buttons:

  • to go to specified screen by name
  • to go to next definied screen
  • to go to previous definied screen
  • to call method definied in current screen
  • to call method definied in other screen
  • to call method definied in screen manager
  • to call method definied in app

On top of screenssample.kv file there is a screens hierarchy. Class MyScreens is a screen manager, parent of three screens: MyScreen1, MyScreen2, MyScreen3.
To go to another screen you have to assign it’s screen name to manager’s current attribute manager.current.

main.py

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager
from kivy.uix.screenmanager import Screen

# hierarhy:
#   ScreensSample (App)
#   |- MyScreens (ScreenManager)
#      |- MyScreen1 (Screen)
#      |- MyScreen2 (Screen)
#      |- MyScreen3 (Screen)

class MyScreens(ScreenManager):
    def screen_manager_method(self):
        print('Hello from screen manager')

class MyScreen1(Screen):
    def screen_method(self):
        print('Hello from screen 1')

class MyScreen2(Screen):
    def screen_method(self):
        print('Hello from screen 2')

class MyScreen3(Screen):
    def screen_method(self):
        print('Hello from screen 3')

class ScreensSample(App):
    def app_method(self):
        print('Hello from app')

ScreensSample().run()

screenssample.kv

MyScreens:
    MyScreen1:
    MyScreen2:
    MyScreen3:

<MyScreen1>:
    name: 'screen_one'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 2'
            on_release: root.manager.current = 'screen_two'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 2)'
            on_release: root.manager.get_screen('screen_two').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

<MyScreen2>:
    name: 'screen_two'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 3'
            on_release: root.manager.current = 'screen_three'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 1)'
            on_release: root.manager.get_screen('screen_one').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

<MyScreen3>:
    name: 'screen_three'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 1'
            on_release: root.manager.current = 'screen_one'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 1)'
            on_release: root.manager.get_screen('screen_one').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

I hope this sample is easy to understand. But even based on this simple py and kv file you may notice that having all screens within one kv file and all screen classes within one py file will produce huge files hard to develop and maintain.

Below is same example but every screen is definied within its separate kv and py files. I think it makes code more readable, easier to develop/maintain.

main.py

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager
import screen1, screen2, screen3

# hierarhy:
#   ScreensSample (App)
#   |- MyScreens (ScreenManager)
#      |- MyScreen1 (Screen)
#      |- MyScreen2 (Screen)
#      |- MyScreen3 (Screen)

class MyScreens(ScreenManager):
    def screen_manager_method(self):
        print('Hello from screen manager')

class ScreensSample(App):
    def app_method(self):
        print('Hello from app')

ScreensSample().run()

screenssample.kv

#:include screen1.kv
#:include screen2.kv
#:include screen3.kv

MyScreens:
    MyScreen1:
    MyScreen2:
    MyScreen3:

screen1.py

from kivy.app import App
from kivy.uix.screenmanager import Screen

class MyScreen1(Screen):
    def screen_method(self):
        print('Hello from screen 1')
        # self.manager.get_screen('screen_two').screen_method()  # sample how to call other screen method from here
        # self.manager.screen_manager_method()  # sample how to call screen manager method from here
        # App.get_running_app().app_method()  # sample hot to call app method from here

screen1.kv

<MyScreen1>:
    name: 'screen_one'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 2'
            on_release: root.manager.current = 'screen_two'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 2)'
            on_release: root.manager.get_screen('screen_two').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

screen2.py

from kivy.uix.screenmanager import Screen

class MyScreen2(Screen):
    def screen_method(self):
        print('Hello from screen 2')

screen2.kv

<MyScreen2>:
    name: 'screen_two'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 3'
            on_release: root.manager.current = 'screen_three'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 1)'
            on_release: root.manager.get_screen('screen_one').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

screen3.py

from kivy.uix.screenmanager import Screen

class MyScreen3(Screen):
    def screen_method(self):
        print('Hello from screen 3')

screen3.kv

<MyScreen3>:
    name: 'screen_three'

    BoxLayout:
        orientation: 'vertical'

        Label:
            text: f'I am {root.name}'
        Button:
            text: 'Go to screen 1'
            on_release: root.manager.current = 'screen_one'
        Button:
            text: 'Go to next screen'
            on_release: root.manager.current = root.manager.next()
        Button:
            text: 'Go to previous screen'
            on_release: root.manager.current = root.manager.previous()
        Button:
            text: 'This screen method'
            on_release: root.screen_method()
        Button:
            text: 'Other screen method (from screen 1)'
            on_release: root.manager.get_screen('screen_one').screen_method()
        Button:
            text: 'Screen manager method'
            on_release: root.manager.screen_manager_method()
        Button:
            text: 'App method'
            on_release: app.app_method()

Additionally within screen1.py I included how to access same methods which are accessed from kv files but from Python’s screen method. As you may notice there is small difference how to access method (or attribute) from kv file and python code, like:

kv: root.manager.get_screen('screen_two').screen_method()
python: self.manager.get_screen('screen_two').screen_method()

kv: root.manager.screen_manager_method()
python: self.manager.screen_manager_method()

kv: app.app_method()
python: App.get_running_app().app_method()
Answered By: MST
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.