Kivy: How to bind dropdown opening properly?

Tags: , , ,



I am struggling with making DropDown opening bind properly to the releasing of the selection button. I tried using this:

selection_button.bind(on_release=drpdn.open)

The binding seems to work rather randomly when I create more than one DropDown (some DropDowns open others do not and I cannot see any logic in it). However it shown exactly like this in the example in Kivy documentation.

I was able to overcome this issue by creating my own callback function for DropDown opening where I pass the selection button and the DropDown instance (using functools.partial) and open the DropDown simply by:

def open_dropdown(self, wid, drpdn, *largs):
    drpdn.open(wid)

Although this workaround works, it really bothers me since I evidently do not correctly understand how binding and callbacks work. Can anyone point me in the right direction? I expected that selection_button.bind(on_release=drpdn.open) passes the seletion button to DropDown open method and the DropDown opens on it but evidently it does not. The most mindboggling thing is that it seems to randomly work (e.g. when I create four selection buttons, some of them will work).

This is my test app code. create_dropdown method works flawlessly whereas create_dropdown2 method causes strange behavior (only the last DropDown actually opens).

from functools import partial
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout


class TestApp(App):

    def create_dropdown(self, text, options):
        selection_button = Button(text=text)
        drpdn = DropDown()
        for o in options:
            btn = Button(text=o, size_hint_y=None, height=44)
            btn.bind(on_release=partial(self.change_select,
                                        selection_button,
                                        btn.text
                                        )
                     )
            btn.bind(on_release=drpdn.dismiss)
            drpdn.add_widget(btn)
        selection_button.bind(on_release=partial(self.open_dropdown,
                                                 selection_button,
                                                 drpdn
                                                 )
                              )
        return selection_button

    def create_dropdown2(self, text, options):
        selection_button = Button(text=text)
        drpdn = DropDown()
        for o in options:
            btn = Button(text=o, size_hint_y=None, height=44)
            btn.bind(on_release=partial(self.change_select,
                                        selection_button,
                                        btn.text
                                        )
                     )
            btn.bind(on_release=drpdn.dismiss)
            drpdn.add_widget(btn)
        selection_button.bind(on_release=drpdn.open)
        return selection_button

    def change_select(self, wid, text, *largs):
        wid.text = text

    def open_dropdown(self, wid, drpdn, *largs):
        drpdn.open(wid)

    def build(self):
        root = BoxLayout(orientation="vertical")

        for _ in range(3):
            root.add_widget(self.create_dropdown("Works",
                                                 ["yes", "no"]
                                                 )
                            )

        for _ in range(4):
            root.add_widget(self.create_dropdown2("Does not work",
                                                  ["yes", "no"]
                                                  )
                            )

        return root


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

Answer

In your create_dropdown2() method you are binding on_release to a local variable (drpdn). The binding creates a weak reference to drpdn. That means that when drpdn goes out of scope, the binding will not prevent it from being deleted and garbage collected. Then, when the button is released, the method that the on_release is bound to is now None.

The partial call in create_dropdown() freezes the supplied args and avoids the out of scope issue.

Note that in the documentation, the dropdown is not a local variable.



Source: stackoverflow