Search code examples
pythonkivyloadingtextinputstoring-data

Python kivy (kivymd) how to store and load data from MD


I want to store data and load data from a TextField (here MDTextField from kivymd). I have added on_application_close: ... and on_application_open: ... just as prototypes, because I don't know how to do it and how to wire it up to the main.py file which contains the functions. So I want the user to be able to input some values into the TextField and if the user closes and reopens the applications these values should still be persisting. I looked up the storage function in the documentation, but it is not clear to me how I can use it properly.

MDTextField:
    id: first_text_field
    hint_text: "Helper text on focus"
    helper_text: "This will disappear when you click off"
    helper_text_mode: "on_focus"
    input_filter: "int"
    on_application_close: store_content(first_text_field, first_text_field.text)
    on_application_open: set_text(load_content(first_text_field))

EDIT: Here is the updated code (main.py, main.kv and labels.py). All files are inside the folder application of the project folder. I also added an empty myBackup.json to the same folder.

Here is the labels.py file

# labels.py
APPLICATION_NAME = "Application"
CLOSE_APPLICATION = "Close Application"
NAVIGATION = "Navigation"
SYSTEM_PARAMETERS = "System Parameters"
TEXT_ON_CLOSE_APPLICATION = "Are your sure?"
TEXT_ON_CLOSE_APPLICATION_BUTTON_CLOSE = "Close Application"

CONTROLLER_PARAMETER_1_HINT_TEXT = "Controller Parameter 1"
CONTROLLER_PARAMETER_1_HELPER_TEXT = "This parameter controls damping ratio."
CONTROLLER_PARAMETER_2_HINT_TEXT = "Controller Parameter 2"
CONTROLLER_PARAMETER_2_HELPER_TEXT = "This parameter controls P component of the PID controller."

Updated code: I don't get an error, but the myBackup.json file stays empty after exiting the application via the close button (X Button) of the window.

# main.py
# -*- coding: utf-8 -*-

import os

# Set virtual keyboard
from kivy.app import App
from kivy.config import Config
from kivy.storage.jsonstore import JsonStore
from kivy.uix.boxlayout import BoxLayout

Config.set('kivy', 'keyboard_mode', 'systemandmulti')
from kivy.uix.popup import Popup
from kivy.uix.button import Button


from kivy.core.window import Window
# Window.fullscreen = "auto"
from kivy.lang import Builder
from functools import partial


from kivymd.list import BaseListItem
from kivymd.material_resources import DEVICE_TYPE
from kivymd.navigationdrawer import MDNavigationDrawer, NavigationDrawerHeaderBase
from kivymd.theming import ThemeManager

from application.labels import *


class HackedDemoNavDrawer(MDNavigationDrawer):
    # DO NOT USE
    def add_widget(self, widget, index=0):
        if issubclass(widget.__class__, BaseListItem):
            self._list.add_widget(widget, index)
            if len(self._list.children) == 1:
                widget._active = True
                self.active_item = widget
            # widget.bind(on_release=lambda x: self.panel.toggle_state())
            widget.bind(on_release=lambda x: x._set_active(True, list=self))
        elif issubclass(widget.__class__, NavigationDrawerHeaderBase):
            self._header_container.add_widget(widget)
        else:
            super(MDNavigationDrawer, self).add_widget(widget, index)


class MainApp(App):

    theme_cls = ThemeManager()
    title = APPLICATION_NAME

    def build(self):
        main_widget = Builder.load_file(
            os.path.join(os.path.dirname(__file__), "./main.kv")
        )
        self.theme_cls.theme_style = 'Dark'

        main_widget.ids.text_field_error.bind(
            on_text_validate=self.set_error_message,
            on_focus=self.set_error_message)
        self.bottom_navigation_remove_mobile(main_widget)
        return main_widget

    def stop(self, *largs):
        # Open the popup you want to open and declare callback if user pressed `Yes`
        popup = ExitPopup(title=TEXT_ON_CLOSE_APPLICATION,
                          content=Button(text=TEXT_ON_CLOSE_APPLICATION_BUTTON_CLOSE),
                          size=(400, 400), size_hint=(None, None)
                          )
        popup.bind(on_confirm=partial(self.close_app, *largs))
        popup.open()

    def close_app(self, *largs):
        super(MainApp, self).stop(*largs)

    def bottom_navigation_remove_mobile(self, widget):
        # Removes some items from bottom-navigation demo when on mobile
        if DEVICE_TYPE == 'mobile':
            widget.ids.bottom_navigation_demo.remove_widget(widget.ids.bottom_navigation_desktop_2)
        if DEVICE_TYPE == 'mobile' or DEVICE_TYPE == 'tablet':
            widget.ids.bottom_navigation_demo.remove_widget(widget.ids.bottom_navigation_desktop_1)

    def set_error_message(self, *args):
        if len(self.root.ids.text_field_error.text) == 2:
            self.root.ids.text_field_error.error = True
        else:
            self.root.ids.text_field_error.error = False

    def on_pause(self):
        return True

    def on_text_validate_callback(self, instance):
        print(instance.text)

    def on_start(self):
        print("\non_start:")
        store = JsonStore('myBackup.json')
        if store.count() > 0:
            for key in store:
                print("\tid={0}, obj={1}".format(key, self.root.ids[key]))
                if isinstance(self.root.ids[key], MDTextField):
                    self.root.ids[key].text = store.get(key)['text']
                    print("\t\ttext=", self.root.ids[key].text)

    def on_stop(self):
        print("\non_stop:")
        store = JsonStore('myBackup.json')
        for key in self.root.ids:
            if isinstance(self.root.ids[key], MDTextField):
                print("\tid={0}, text={1}".format(key, self.root.ids[key].text))
                store.put(key, text=self.root.ids[key].text)


class ExitPopup(Popup):

    def __init__(self, **kwargs):
        super(ExitPopup, self).__init__(**kwargs)
        self.register_event_type('on_confirm')

    def on_confirm(self):
        pass

    def on_button_yes(self):
        self.dispatch('on_confirm')


if __name__ == '__main__':
    MainApp().run()

Here is the main.kv file. The labels.py file is unchanged

# main.kv
#:kivy 1.10.1
#:import Toolbar kivymd.toolbar.Toolbar
#:import MDNavigationDrawer application.kivymd.navigationdrawer.MDNavigationDrawer
#:import NavigationLayout application.kivymd.navigationdrawer.NavigationLayout
#:import NavigationDrawerDivider application.kivymd.navigationdrawer.NavigationDrawerDivider
#:import NavigationDrawerToolbar application.kivymd.navigationdrawer.NavigationDrawerToolbar
#:import NavigationDrawerSubheader application.kivymd.navigationdrawer.NavigationDrawerSubheader
#:import MDTextField application.kivymd.textfields.MDTextField
#:import labels application.labels

NavigationLayout:
    id: nav_layout
    MDNavigationDrawer:
        id: nav_drawer
        NavigationDrawerToolbar:
            title: labels.NAVIGATION
        NavigationDrawerIconButton:
            icon: 'checkbox-blank-circle'
            text: labels.SYSTEM_PARAMETERS
            on_release: app.root.ids.scr_mngr.current = 'system_parameters'
        NavigationDrawerIconButton:
            icon: "checkbox-blank-circle"
            text: labels.CLOSE_APPLICATION
            on_release: app.stop()
    BoxLayout:
        orientation: 'vertical'
        halign: "center"
        Toolbar:
            id: toolbar
            title: labels.APPLICATION_NAME
            md_bg_color: app.theme_cls.primary_color
            background_palette: 'Primary'
            background_hue: '500'
            left_action_items: [['menu', lambda x: app.root.toggle_nav_drawer()]]
            #right_action_items: [['dots-vertical', lambda x: app.root.toggle_nav_drawer()]]
        ScreenManager:
            id: scr_mngr
            Screen:
                name: 'system_parameters'
                BoxLayout:
                    orientation: "horizontal"
                    BoxLayout:
                        orientation: 'vertical'
                        size_hint_y: None
                        height: self.minimum_height
                        padding: dp(48)
                        spacing: 10
                        MDTextField:
                            id: controller_parameter_1
                            hint_text: labels.CONTROLLER_PARAMETER_1_HINT_TEXT
                            helper_text: labels.CONTROLLER_PARAMETER_1_HELPER_TEXT
                            helper_text_mode: "on_focus"
                            input_filter: "int"
                            on_text_validate: app.on_text_validate_callback(self)
                    BoxLayout:
                        orientation: 'vertical'
                        size_hint_y: None
                        height: self.minimum_height
                        padding: dp(48)
                        spacing: 10
                        MDTextField:
                            id: controller_parameter_2
                            hint_text: labels.CONTROLLER_PARAMETER_2_HINT_TEXT
                            helper_text: labels.CONTROLLER_PARAMETER_2_HELPER_TEXT
                            helper_text_mode: "on_focus"
                            input_filter: "float"
                            on_text_validate: app.on_text_validate_callback(self)

            Screen:
                name: 'textfields'
                ScrollView:
                    BoxLayout:
                        orientation: 'vertical'
                        MDTextField:
                            id: text_field_error
                            hint_text: "Helper text on error (Hit Enter with two characters here)"
                            helper_text: "Two is my least favorite number"
                            helper_text_mode: "on_error"

            Screen:
                name: 'nav_drawer'
                HackedDemoNavDrawer:
                    # NavigationDrawerToolbar:
                    #     title: "Navigation Drawer Widgets"
                    NavigationDrawerIconButton:
                        icon: 'checkbox-blank-circle'
                        text: "Badge text ---->"
                        badge_text: "99+"
                    NavigationDrawerIconButton:
                        active_color_type: 'accent'
                        text: "Accent active color"
                    NavigationDrawerIconButton:
                        active_color_type: 'custom'
                        text: "Custom active color"
                        active_color: [1, 0, 1, 1]
                    NavigationDrawerIconButton:
                        use_active: False
                        text: "Use active = False"
                    NavigationDrawerIconButton:
                        text: "Different icon"
                        icon: 'alarm'
                    NavigationDrawerDivider:
                    NavigationDrawerSubheader:
                        text: "NavigationDrawerSubheader"
                    NavigationDrawerIconButton:
                        text: "NavigationDrawerDivider \/"
                    NavigationDrawerDivider:

Solution

  • Question 2

    I don't get an error, but the myBackup.json file stays empty after exiting the application via the close button (X Button) of the window.

    Solution 2

    Use Window.bind(on_close=self.on_stop)

    Example 2

    app.py

    # app.py
    # -*- coding: utf-8 -*-
    
    import os
    from functools import partial
    
    from kivy.app import App
    from kivy.storage.jsonstore import JsonStore
    from kivy.uix.popup import Popup
    from kivy.uix.button import Button
    from kivy.lang import Builder
    
    from kivy.config import Config
    
    # Set virtual keyboard
    Config.set('kivy', 'keyboard_mode', 'systemandmulti')
    
    from kivy.core.window import Window
    # Window.fullscreen = "auto"
    
    from kivymd.list import BaseListItem
    from kivymd.material_resources import DEVICE_TYPE
    from kivymd.navigationdrawer import MDNavigationDrawer, NavigationDrawerHeaderBase
    from kivymd.textfields import MDTextField
    from kivymd.theming import ThemeManager
    
    from labels import *
    
    
    class HackedDemoNavDrawer(MDNavigationDrawer):
        # DO NOT USE
        def add_widget(self, widget, index=0):
            if issubclass(widget.__class__, BaseListItem):
                self._list.add_widget(widget, index)
                if len(self._list.children) == 1:
                    widget._active = True
                    self.active_item = widget
                # widget.bind(on_release=lambda x: self.panel.toggle_state())
                widget.bind(on_release=lambda x: x._set_active(True, list=self))
            elif issubclass(widget.__class__, NavigationDrawerHeaderBase):
                self._header_container.add_widget(widget)
            else:
                super(MDNavigationDrawer, self).add_widget(widget, index)
    
    
    class MainApp(App):
    
        theme_cls = ThemeManager()
        title = APPLICATION_NAME
    
        def __init__(self, **kwargs):
            super(MainApp, self).__init__(**kwargs)
            Window.bind(on_close=self.on_stop)
    
        def build(self):
            main_widget = Builder.load_file(
                os.path.join(os.path.dirname(__file__), "./main.kv")
            )
            self.theme_cls.theme_style = 'Dark'
    
            main_widget.ids.text_field_error.bind(
                on_text_validate=self.set_error_message,
                on_focus=self.set_error_message)
            self.bottom_navigation_remove_mobile(main_widget)
            return main_widget
    
        def stop(self, *largs):
            # Open the popup you want to open and declare callback if user pressed `Yes`
            popup = ExitPopup(title=TEXT_ON_CLOSE_APPLICATION,
                              content=Button(text=TEXT_ON_CLOSE_APPLICATION_BUTTON_CLOSE),
                              size=(400, 400), size_hint=(None, None)
                              )
            popup.bind(on_confirm=partial(self.close_app, *largs))
            popup.open()
    
        def close_app(self, *largs):
            super(MainApp, self).stop(*largs)
    
        def bottom_navigation_remove_mobile(self, widget):
            # Removes some items from bottom-navigation demo when on mobile
            if DEVICE_TYPE == 'mobile':
                widget.ids.bottom_navigation_demo.remove_widget(widget.ids.bottom_navigation_desktop_2)
            if DEVICE_TYPE == 'mobile' or DEVICE_TYPE == 'tablet':
                widget.ids.bottom_navigation_demo.remove_widget(widget.ids.bottom_navigation_desktop_1)
    
        def set_error_message(self, *args):
            if len(self.root.ids.text_field_error.text) == 2:
                self.root.ids.text_field_error.error = True
            else:
                self.root.ids.text_field_error.error = False
    
        def on_pause(self):
            return True
    
        def on_text_validate_callback(self, instance):
            print(instance.text)
    
        def on_start(self):
            print("\non_start:")
            store = JsonStore('myBackup.json')
            if store.count() > 0:
                for key in store:
                    print("\tid={0}, obj={1}".format(key, self.root.ids[key]))
                    if isinstance(self.root.ids[key], MDTextField):
                        self.root.ids[key].text = store.get(key)['text']
                        print("\t\ttext=", self.root.ids[key].text)
    
        def on_stop(self, *args):
            print("\non_stop:")
            store = JsonStore('myBackup.json')
            for key in self.root.ids:
                if isinstance(self.root.ids[key], MDTextField):
                    print("\tid={0}, text={1}".format(key, self.root.ids[key].text))
                    store.put(key, text=self.root.ids[key].text)
    
    
    class ExitPopup(Popup):
    
        def __init__(self, **kwargs):
            super(ExitPopup, self).__init__(**kwargs)
            self.register_event_type('on_confirm')
    
        def on_confirm(self):
            pass
    
        def on_button_yes(self):
            self.dispatch('on_confirm')
    
    
    if __name__ == '__main__':
        MainApp().run()
    

    main.kv

    #:kivy 1.11.0
    #:import Toolbar kivymd.toolbar.Toolbar
    #:import MDNavigationDrawer kivymd.navigationdrawer.MDNavigationDrawer
    #:import NavigationLayout kivymd.navigationdrawer.NavigationLayout
    #:import NavigationDrawerDivider kivymd.navigationdrawer.NavigationDrawerDivider
    #:import NavigationDrawerToolbar kivymd.navigationdrawer.NavigationDrawerToolbar
    #:import NavigationDrawerSubheader kivymd.navigationdrawer.NavigationDrawerSubheader
    #:import MDTextField kivymd.textfields.MDTextField
    #:import labels labels
    
    NavigationLayout:
        id: nav_layout
        MDNavigationDrawer:
            id: nav_drawer
            NavigationDrawerToolbar:
                title: labels.NAVIGATION
            NavigationDrawerIconButton:
                icon: 'checkbox-blank-circle'
                text: labels.SYSTEM_PARAMETERS
                on_release: app.root.ids.scr_mngr.current = 'system_parameters'
            NavigationDrawerIconButton:
                icon: "checkbox-blank-circle"
                text: labels.CLOSE_APPLICATION
                on_release: app.stop()
        BoxLayout:
            orientation: 'vertical'
            halign: "center"
            Toolbar:
                id: toolbar
                title: labels.APPLICATION_NAME
                md_bg_color: app.theme_cls.primary_color
                background_palette: 'Primary'
                background_hue: '500'
                left_action_items: [['menu', lambda x: app.root.toggle_nav_drawer()]]
                #right_action_items: [['dots-vertical', lambda x: app.root.toggle_nav_drawer()]]
            ScreenManager:
                id: scr_mngr
                Screen:
                    name: 'system_parameters'
                    BoxLayout:
                        orientation: "horizontal"
                        BoxLayout:
                            orientation: 'vertical'
                            size_hint_y: None
                            height: self.minimum_height
                            padding: dp(48)
                            spacing: 10
                            MDTextField:
                                id: controller_parameter_1
                                hint_text: labels.CONTROLLER_PARAMETER_1_HINT_TEXT
                                helper_text: labels.CONTROLLER_PARAMETER_1_HELPER_TEXT
                                helper_text_mode: "on_focus"
                                input_filter: "int"
                                on_text_validate: app.on_text_validate_callback(self)      # Fix 1
                        BoxLayout:
                            orientation: 'vertical'
                            size_hint_y: None
                            height: self.minimum_height
                            padding: dp(48)
                            spacing: 10
                            MDTextField:
                                id: controller_parameter_2
                                hint_text: labels.CONTROLLER_PARAMETER_2_HINT_TEXT
                                helper_text: labels.CONTROLLER_PARAMETER_2_HELPER_TEXT
                                helper_text_mode: "on_focus"
                                input_filter: "float"
                                on_text_validate: app.on_text_validate_callback(self)      # Fix 2
    
                Screen:
                    name: 'textfields'
                    ScrollView:
                        BoxLayout:
                            orientation: 'vertical'
                            MDTextField:
                                id: text_field_error
                                hint_text: "Helper text on error (Hit Enter with two characters here)"
                                helper_text: "Two is my least favorite number"
                                helper_text_mode: "on_error"
    
                Screen:
                    name: 'nav_drawer'
                    HackedDemoNavDrawer:
                        # NavigationDrawerToolbar:
                        #     title: "Navigation Drawer Widgets"
                        NavigationDrawerIconButton:
                            icon: 'checkbox-blank-circle'
                            text: "Badge text ---->"
                            badge_text: "99+"
                        NavigationDrawerIconButton:
                            active_color_type: 'accent'
                            text: "Accent active color"
                        NavigationDrawerIconButton:
                            active_color_type: 'custom'
                            text: "Custom active color"
                            active_color: [1, 0, 1, 1]
                        NavigationDrawerIconButton:
                            use_active: False
                            text: "Use active = False"
                        NavigationDrawerIconButton:
                            text: "Different icon"
                            icon: 'alarm'
                        NavigationDrawerDivider:
                        NavigationDrawerSubheader:
                            text: "NavigationDrawerSubheader"
                        NavigationDrawerIconButton:
                            text: "NavigationDrawerDivider \/"
                        NavigationDrawerDivider:
    

    Output

    Img01 - Close Window Img02 - Restart App


    Please refer to example for details.

    1. Use Kivy Storage
    2. Use Application events, on_start and on_stop.
    3. Add import statement, from kivymd.textfields import MDTextField

    App events

    on_start:
    

    Fired when the application is being started (before the runTouchApp() call.

    on_stop:
    

    Fired when the application stops.

    Example

    main.py

    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivymd.theming import ThemeManager
    from kivymd.textfields import MDTextField
    from kivy.storage.jsonstore import JsonStore
    
    
    class RootWidget(BoxLayout):
         def on_text_validate_callback(self, instance):
             print(instance.text)
    
    
    class TestApp(App):
        theme_cls = ThemeManager()
    
        def build(self):
            return RootWidget()
    
        def on_start(self):
            print("\non_start:")
            store = JsonStore('myBackup.json')
            if store.count() > 0:
                for key in store:
                    print("\tid={0}, obj={1}".format(key, self.root.ids[key]))
                    if isinstance(self.root.ids[key], MDTextField):
                        self.root.ids[key].text = store.get(key)['text']
                        print("\t\ttext=", self.root.ids[key].text)
    
        def on_stop(self):
            print("\non_stop:")
            store = JsonStore('myBackup.json')
            for key in self.root.ids:
                if isinstance(self.root.ids[key], MDTextField):
                    print("\tid={0}, text={1}".format(key, self.root.ids[key].text))
                    store.put(key, text=self.root.ids[key].text)
    
    
    if __name__ == "__main__":
        TestApp().run()
    

    test.kv

    #:kivy 1.11.0
    #:import MDTextField kivymd.textfields.MDTextField
    
    <RootWidget>:
        MDTextField:
            id: first_text_field
            color: 0, 0, 0, 1    # black text color
            focus: True
            hint_text: "Helper text on focus"
            helper_text: "This will disappear when you click off"
            helper_text_mode: "on_focus"
            input_filter: "int"
            on_text_validate: root.on_text_validate_callback(self)
    

    Output

    Img01 - on_start: Empty TextInput Img02 - Entered text Img03 - on_stop Img04 - on_start: restore previous value