Search code examples
pythondrop-down-menukivy

Multiple drop down lists using Kivy library


I want to have multiple dropdown lists as a part of my application but some of them do not work and I don't know why. The code for that is:

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

class MidTopBar():
    def __init__(self):

        self.dropDownLabels = {
            "File"  : ['Example1', 'Example2','Example3', 'Example4'],
            "Edit"  : ['Example5', 'Example6','Example7', 'Example8'],
            "View"  : ['Example9', 'Example10','Example11', 'Example12'],
            "Help"  : ['Example13', 'Example14','Example15', 'Example16'],
        }
        self.generate()

         
    def _generateDropdown(self, name, list):
        dropdown = DropDown(auto_width = True,
                            width = 300)

        for index in range(len(list)):
            btn = Button(text=list[index],
                         halign = 'left',
                         valign = 'center',
                         padding_x= 20,
                         size_hint_y=None,
                         background_color = (1, 1, 1, 1))

            btn.bind(on_release=lambda btn: dropdown.select(list[index]))
            btn.bind(size= btn.setter('text_size'))
            dropdown.add_widget(btn)

        mainbutton = Button(text = name)

        dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
        mainbutton.bind(on_release=dropdown.open)

        return mainbutton
    
    def _dropDowns(self):
        dropDownList = []
        for key in self.dropDownLabels:
            dropDownList.append(self._generateDropdown(name= key, list = self.dropDownLabels[key]))
        return dropDownList
                
    def generate(self):
        dropDownList = self._dropDowns()
        self.Box = BoxLayout()
        for dropDown in dropDownList:
            self.Box.add_widget(dropDown)
        self.emptyBox = BoxLayout()
        self.superBox = BoxLayout(orientation='vertical')
        self.superBox.add_widget(self.Box)
        self.superBox.add_widget(self.emptyBox)
        
class Example(App):
    
    def build(self):
        midTopBar       :object = MidTopBar().superBox
        return midTopBar

# Run the App of TextInput Widget in kivy
if __name__ == "__main__":
    Example().run()

I'm expecting all 4 lists in the constructor to be shown in their respective key buttons but only one or occasionally 2 works.


Solution

  • I believe you are suffering from trash collection. Python automatically frees objects that have no saved reference in the code. Examples are your MidTopBar instance in your build() method, and the dropdown in your _generateDropdown() method. I have modified your code to save references to those objects, and it seems to fix it. First, I modified your build() method:

    def build(self):
        # midTopBar: object = MidTopBar().superBox
        # return midTopBar
        self.mtb = MidTopBar()  # prevent trash collection of MidTopBar instance
        return self.mtb.superBox
    

    And added a list to hold references to the DropDown instances. I the 'init()` method:

    class MidTopBar():
        def __init__(self):
            self.dds = []
    

    Then in the _generateDropdown() method, actually save the references:

    def _generateDropdown(self, name, list1):
        dropdown = DropDown(auto_width=True,
                            width=300)
        self.dds.append(dropdown)  # just to protect from trash collection
    

    There is another issue with the code line:

    btn.bind(on_release=lambda btn: dropdown.select(list[index]))
    

    Because of the nature of lambda, this will always result in the last element of the list being selected. To correct this, use a temporary variable:

    btn.bind(on_release=lambda btn, t=text: dropdown.select(t))
    

    This will result in the correct item being selected and shown in the mainbutton.

    This is how Dropdowns are typically used, with the mainbutton showing the current selection. If you don't want the selection shown, you can replace the line:

    btn.bind(on_release=lambda btn, t=text: dropdown.select(t))
    

    with:

    btn.bind(on_release=dropdown.dismiss)