Search code examples
pythonkivykivy-language

Kivy Three Buttons generated on pressing a Button. One of the New Buttons when pressed should remove All 3 New Buttons


In this code, On pressing button "CREATE", three buttons "Remove","Btn1","Btn2" are created... On pressing "Remove" button, all the three buttons ("Remove","Btn1","Btn2") should be removed. I tried many ways but I can't find a way to make the new buttons accessible to other functions that are under the same class.

Main Code:

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen,ScreenManager

class Main(Screen):
    def remove(self):
        self.ids.Fl.remove_widget(Btnremove)
        self.ids.Fl.remove_widget(Btn1)
        self.ids.Fl.remove_widget(Btn2)

    def addme(self):
        Btnremove=Button(text="Remove",font_size=18,size_hint=(.2,.1),pos_hint={"center_x": .5, "center_y": .7})
        Btn1=Button(text= "Btn1",font_size=18,size_hint=(.2,.1),pos_hint={"center_x":.4,"center_y":.2})
        Btn2=Button(text="Btn2",font_size=18,size_hint=(.2, .1),pos_hint={"center_x": .6, "center_y": .2})
        self.ids.Fl.add_widget(Btnremove)
        self.ids.Fl.add_widget(Btn1)
        self.ids.Fl.add_widget(Btn2)
        Btnremove.bind(on_press=self.remove())

class Manager(ScreenManager):
    pass
kv=Builder.load_file("test2.kv")
screen=Manager()
screen.add_widget(Main(name="main"))
class Test(App):
    def build(self):
        return screen
Test().run()

Kv Code:

<Main>:
    name: "main"
    FloatLayout:
        id: Fl
        Button:
            id: create
            text: "CREATE"
            size_hint: (.55,.175)
            pos_hint: {"center_x":.5,"center_y":.5}
            on_press:
                root.addme()

Solution

  • The answered solution of Parvat iterates over all children (buttons) inside your widget and removes all of them, that have not the text CREATE. So it implicitly works without knowing the buttons created before.

    Considerations

    Even if this is expected to work, but it does not really explain your issue here. It works only, because it has no knowledge of the buttons. Only thing it assumes, that there is one button CREATE to preserve (not delete). Unfortunately the CREATE button was defined even outside code in the config. If you change the text there (e.g. to CREATE BUTTONS), your function will not work as expected anymore.

    Issues spotted

    Use Instance Variables instead

    You claim that your remove method does not work as expected. And further that you don't know how to make buttons accessible to other functions.

    In you instance method remove(self) you're addressing the button objects (e.g. Btnremove) like they were global variables.

    def remove(self):
        self.ids.Fl.remove_widget(Btnremove)
        self.ids.Fl.remove_widget(Btn1)
        self.ids.Fl.remove_widget(Btn2)
    

    But they aren't. They are local variables, only visible inside the other instance method addme(self):

    def addme(self):
            Btnremove=Button(text="Remove",font_size=18,size_hint=(.2,.1),pos_hint={"center_x": .5, "center_y": .7})
            Btn1=Button(text= "Btn1",font_size=18,size_hint=(.2,.1),pos_hint={"center_x":.4,"center_y":.2})
            Btn2=Button(text="Btn2",font_size=18,size_hint=(.2, .1),pos_hint={"center_x": .6, "center_y": .2})
      chance
    

    Bind a function by name only

    Your bound action for on_press event was defined like calling a function, i.e. self.remove(). Instead define it as reference to the function/method by simply passing the name like self.remove (without parentheses!).

    See Kivy's documentation on Button:

    To attach a callback when the button is pressed (clicked/touched), use bind:

    def callback(instance):
        print('The button <%s> is being pressed' % instance.text)
    
    btn1 = Button(text='Hello world 1')
    btn1.bind(on_press=callback)
    
    

    This callback is simply the method-name (without parentheses). In your case:

    # before: will result in `None` bound and raise an error when pressed
    Btnremove.bind(on_press=self.remove())
    
    # after: will call the function by name
    Btnremove.bind(on_press=self.remove)
    

    Congrats, you found the bug yourself and fixed it.

    What happened before, why the error?

    Because when binding and defining the callback as on_press=self.remove() the method is immediately called and returns None (the method does not return an object). Then None is bound instead of the function-reference.

    So adversely two things happen that you did not expect:

    1. you removed the buttons during binding (before button was pressed)
    2. when button is pressed, the callback defined as None can not be called. Hence the error:

    AssertionError: None is not callable

    Accessing state or variables between instance methods

    There are different ways to solve this (which have nothing to do with Kivy):

    1. add a parameter (holding the buttons) to the method so you can pass over local variables as argument when calling
    2. add instance variables (holding the buttons) so every instance method inside your class has access to them

    Add Parameter to an Instance Method

    # adding a parameter `widgets`
    def remove(self, widgets):
        for w in widgets:
            self.ids.Fl.remove_widget(w)
    
    # calling the method with argument
    def addme(self):
        # omitted code
        buttons_to_remove = [Btnremove, Btn1, Btn2]  # creating a list with your buttons
        Btnremove.bind(on_press=self.remove(buttons_to_remove))  # pass the list as argument to the method
    

    ⚠️ Warning: This binding on_press=self.remove(buttons_to_remove) will not work, since a callback has to be passed as method-reference by name only. No custom arguments can be passed like buttons_to_remove. The only argument that is passed implicitly to the method by Kivy is instance as reference to the button instance itself.

    Add Instance Variables

    Over these instance variables (the instance scope is marked with prefix self.) we can share state inside the object. So we can share state between all instance methods because all have access to the instance object self. So they have also access to everything inside self like self.buttons_to_delete.

    # accessing instance variable `buttons_to_remove`
    def remove(self):
        # accessing the shared instance variable using instance-prefix `self.`
        for btn in self.buttons_to_remove:
            self.ids.Fl.remove_widget(btn)
    
    # adding buttons to the instance-variable `buttons_to_remove`
    def addme(self):
        # omitted code
        self.buttons_to_remove = [Btnremove, Btn1, Btn2]  # creating a list with your buttons
        Btnremove.bind(on_press=self.remove())  # the method has access to this list
    

    ⚠️ Warning: If you haven't declared the instance variable self.buttons_to_remove before (e.g. in constructor), and you now call remove before addme, then inside remove it will raise an error because your instance-variable does not yet exist and is unknown.

    Removing Widgets

    A similar question was already asked and answered: How to address remove_widget what widget to remove inside another layout in Kivy

    The call of Kivy's remove_widget method is right, expects a child (of type Widget or here: Button) passed to it.

    Like in this example from the docs about remove_widget:

    >>> from kivy.uix.button import Button
    >>> root = Widget()
    >>> button = Button()
    >>> root.add_widget(button)
    >>> root.remove_widget(button) 
    

    Notice here, that the local variable button is visible & accessible to the method remove_widget because it's passed as argument.