Search code examples
pythonkivyweak-references

Kivy adding and removing widgets from GridLayout gives weak reference issue


I am attempting to dynamically add and remove widgets from a GridLayout with Python-Kivy, and I am running into an issue with weak references. When the Screen containing the GridLayout is initialized, I am placing a Label inside of the GridLayout that contains text notifying the user that the container has no items (technically, it has the Label in there so it is not necessarily empty). Then I have a Button that allows the user to add individual GridLayout widgets to the GridLayout that contain a Label, a TextInput and a CheckBox, as well as a Button that allows the user to remove those individual GridLayout widgets that they created. If all of these widgets (dynamically added by the user) are removed, then the original Label is added back to the GridLayout.

When I attempt to construct this logic in Python to pair with Kivy, I run into an issue where the original Label never seems to be fully removed from the stack.

I was under the impression that self.ids.widget_list.remove_widget(self.ids.empty) would remove the Label widget with id empty, however this is not the case. It is clear that the widget still exists, when I call print(self.ids):

{'widget_list': <WeakProxy to <kivy.uix.gridlayout.GridLayout object at 0x0451F030>>, 'empty': <WeakProxy to <kivy.uix.gridlayout.GridLayout object at 0x0451F9D0>>}

Any help is much appreciated.

EDIT

When checking for i in self.layouts: print(i.children) every time the remove() method is called, shows that the references to the added widgets are never being completely removed. This may be where my issues resides, but unsure how to resolve it.

Python

import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.checkbox import CheckBox
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import ScreenManager, Screen

#Load kv file
Builder.load_file('test.kv')

#First Screen
class Screen1(Screen):

    layouts = []

    def remove(self):

        for i in self.layouts:
            if i.children[0].active:
                self.ids.widget_list.remove_widget(i)

        if len(self.layouts)==0:
            layout = GridLayout(rows=1, id=empty)
            layout.add_widget(Label(text='Nothing Here'))
            self.ids.widget_list.add_widget(layout)
        else:
            self.update_hints()

    def add(self):

        if len(self.ids.widget_list.children)<5:

            print(self.ids)
            self.ids.widget_list.remove_widget(self.ids.empty)
            print(self.ids)
            layout = GridLayout(cols=3)
            layout.add_widget(Label(text='Test ' + str(len(self.ids.widget_list.children)+1)))
            layout.add_widget(TextInput())
            layout.add_widget(CheckBox())
            self.ids.widget_list.add_widget(layout)

            self.layouts.append(layout)
            self.update_hints()

        else:

            layout = GridLayout(cols=1)
            layout.add_widget(Label(text='Only five allowed at once.\nRemove at least one to add another.'))
            button = Button(text='Acknowledge'); layout.add_widget(button)
            popup = Popup(content=layout, title='Limit Reached', size_hint=(.5,.5), auto_dismiss=False)
            button.bind(on_release=popup.dismiss)
            popup.open()

    def update_hints(self):

        for i in self.layouts:
            i.children[1].hint_text = 'Ex. ' + str(round(100/len(self.ids.widget_list.children),2)) + '%'

#Initialize Screens and Start App
class MyScreenManager(ScreenManager):

    pass

#Main application
class SampleApp(App):

    def build(self):
        self.sm = MyScreenManager()
        return self.sm

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

kv

<MyScreenManager>:

    Screen1:
        name: 'screen1'

<Screen1>:

    BoxLayout:

        orientation: 'vertical'

        GridLayout:

            cols: 3
            padding: 20
            spacing: 20
            size_hint: 1, .1

            Label:
                text: 'List of Widgets'
                underline: True
            Label:
                text: 'Percentage'
                underline: True
            Label:
                text: 'Remove (Y/N)'
                underline: True

        ScrollView:

            size_hint: 1, .5
            do_scroll_x: False
            padding: 20
            spacing: 20

            GridLayout:

                id: widget_list
                cols: 1
                spacing: 5

                GridLayout:
                    id: empty
                    rows: 1
                    Label:
                        text: 'Nothing Here'

        GridLayout:

            cols: 3
            padding: 20
            spacing: 20
            size_hint: 1, .2

            Button:
                text: 'Add Widget'
                on_release: root.add()

            Label:
                text: ''

            Button:
                text: 'Remove Widget'
                on_release: root.remove()

Solution

  • Ok, I seem to have found a simpler means of implementation that avoids specifying any id for the initial Label (the same one that shows if self.layouts==[]):

    Python

    import kivy
    from kivy.app import App
    from kivy.lang import Builder
    from kivy.uix.gridlayout import GridLayout
    from kivy.uix.button import Button
    from kivy.uix.textinput import TextInput
    from kivy.uix.scrollview import ScrollView
    from kivy.uix.label import Label
    from kivy.uix.checkbox import CheckBox
    from kivy.uix.popup import Popup
    from kivy.uix.screenmanager import ScreenManager, Screen
    
    #Load kv file
    Builder.load_file('test.kv')
    
    #First Screen
    class Screen1(Screen):
    
        count = 0
        layouts = []
    
        def remove(self):
    
            for i in self.layouts:
                if i.children[0].active:
                    self.ids.widget_list.remove_widget(i)
    
            self.layouts = [i for i in self.layouts if not i.children[0].active]
    
            if self.layouts!=[]:
                self.update_hints()
            else:
                layout = GridLayout(rows=1)
                layout.add_widget(Label(text='Nothing Here'))
                self.ids.widget_list.add_widget(layout)
    
        def add(self):
    
            if self.layouts==[]:
                self.ids.widget_list.clear_widgets()
    
            if len(self.ids.widget_list.children)<5:
                self.count+=1
                layout = GridLayout(cols=3)
                layout.add_widget(Label(text='Test ' + str(self.count)))
                layout.add_widget(TextInput())
                layout.add_widget(CheckBox())
                self.ids.widget_list.add_widget(layout)
                self.layouts.append(layout)
                self.update_hints()
            else:
                layout = GridLayout(cols=1)
                layout.add_widget(Label(text='Only five allowed at once.\nRemove at least one to add another.'))
                button = Button(text='Acknowledge'); layout.add_widget(button)
                popup = Popup(content=layout, title='Limit Reached', size_hint=(.5,.5), auto_dismiss=False)
                button.bind(on_release=popup.dismiss)
                popup.open()
    
        def update_hints(self):
    
            for i in self.layouts:
                i.children[1].hint_text = 'Ex. ' + str(round(100/len(self.ids.widget_list.children),2)) + '%'
    
    #Initialize Screens and Start App
    class MyScreenManager(ScreenManager):
    
        pass
    
    #Main application
    class SampleApp(App):
    
        def build(self):
            self.sm = MyScreenManager()
            return self.sm
    
    if __name__ == '__main__':
        SampleApp().run()
    

    kv

    <MyScreenManager>:
    
        Screen1:
            name: 'screen1'
    
    <Screen1>:
    
        BoxLayout:
    
            orientation: 'vertical'
    
            GridLayout:
    
                cols: 3
                padding: 20
                spacing: 20
                size_hint: 1, .1
    
                Label:
                    text: 'List of Widgets'
                    underline: True
                Label:
                    text: 'Percentage'
                    underline: True
                Label:
                    text: 'Remove (Y/N)'
                    underline: True
    
            ScrollView:
    
                size_hint: 1, .5
                do_scroll_x: False
                padding: 20
                spacing: 20
    
                GridLayout:
    
                    id: widget_list
                    cols: 1
                    spacing: 5
    
                    GridLayout:
                        rows: 1
                        Label:
                            text: 'Nothing Here'
    
            GridLayout:
    
                cols: 3
                padding: 20
                spacing: 20
                size_hint: 1, .2
    
                Button:
                    text: 'Add Widget'
                    on_release: root.add()
    
                Label:
                    text: ''
    
                Button:
                    text: 'Remove Widget'
                    on_release: root.remove()