Search code examples
pythondrop-down-menukivydropdown

kivy dropdown menu in FloatLayout and GridLayout doesnt show up


Hey guys trying to create an app with a dropdown button but it seems to not work (the full app has GridLayout with 7 rows and this FloatLayout in the example is one of the rows) I've tried with GridLayout, BoxLayout, and FloatLayout and still doesn't appear on the app. Any ideas what's wrong here?

.py file

class WindowManager(ScreenManager, Screen):
    TestMe = ObjectProperty(None)

text_lists = ['hi', 'nice one', 'another one']


class TestMe(Screen, FloatLayout):
    global text_lists
    main_button = ObjectProperty(None)
    selected_list = 'SELECTED'
    top_layout = ObjectProperty(None)
    top_layout = GridLayout(cols=4)

    def __init__(self, **kwargs):
        super(TestMe, self).__init__(**kwargs)
        self.dropdown = DropDown()
        self.create_drop_down()
        self.create_go_button()

    def create_drop_down(self):
        for list_name in text_lists:
            # When adding widgets, we need to specify the height manually
            # (disabling the size_hint_y) so the dropdown can calculate
            # the area it needs.

            btn = Button(text= list_name, size_hint_y=None, height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))

            # for each button, attach a callback that will call the select() method
            # on the dropdown. We'll pass the text of the button as the data of the
            # selection.
            btn.bind(on_release=lambda btn: self.dropdown.select(btn.text),
                     on_press=lambda btn: self.select_list(btn.text))

            # then add the button inside the dropdown
            self.dropdown.add_widget(btn)

        # create a big main button
        self.main_button = Button(text='Choose A List', size_hint=(None, None), height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))

        # show the dropdown menu when the main button is released
        # note: all the bind() calls pass the instance of the caller (here, the
        # mainbutton instance) as the first argument of the callback (here,
        # dropdown.open.).
        self.main_button.bind(on_release=self.dropdown.open)

        # one last thing, listen for the selection in the dropdown list and
        # assign the data to the button text.
        self.dropdown.bind(on_select=lambda instance, x: setattr(self.main_button, 'text', x))
        self.top_layout.add_widget(self.main_button)

    def create_go_button(self):
        go_btn = Button(text="Go!", size_hint=(None, None), height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))
        self.top_layout.add_widget(go_btn)

    def select_list(self, selected):
        self.selected_list = selected

class MyTest(App):
    def build(self):
        return kv


if __name__ == '__main__':
    kv = Builder.load_file('test_kv.kv')

    MyTest().run()

test_kv.kv file

WindowManager:
    TestMe:

<TestMe>:
name: "testy"
id: testy
top_layout: top_layout
FloatLayout:
    Label:
        text: 'Test Screen'
        font: 'Aharoni'
        font_size: 24
        pos_hint: {"left": 0.45, "y": 0.45}
    GridLayout:
        pos_hint: {"top": 0.9}
        size_hint: 1, 0.8
        rows: 2
        spacing: 10
        padding: 10
        GridLayout:
            id: top_layout
            cols: 4
            Button:
                text: "Fun!"
            Label:
                text: "This is a test"
        Button:
            text: "Run!"

enter image description here


Solution

  • The problem is that you are calling create_drop_down() and create_go_button() in the __init__() method of the TestMe class. Since you also define a <TestMe>: rule in your kv, there is a conflict. According to the somewhat unclear documentation, the kv rule is applied after the __init__() is run. That means that any Widgets added to TestMe in its __init__() will be overwritten by the Widegets specified in the kv rule. The possible solutions are to add all the children of TestMe in either the __init__() method or in the kv, or to move the adding of Widgets out of the __init__() method. Here is a modified version of your code that does the latter approach:

    class WindowManager(ScreenManager, Screen):
        TestMe = ObjectProperty(None)
    
    text_lists = ['hi', 'nice one', 'another one']
    
    
    class TestMe(Screen, FloatLayout):
        global text_lists
        main_button = ObjectProperty(None)
        selected_list = 'SELECTED'
        top_layout = ObjectProperty(None)
        #top_layout = GridLayout(cols=4)
    
        def __init__(self, **kwargs):
            super(TestMe, self).__init__(**kwargs)
            self.dropdown = DropDown()
            Clock.schedule_once(self.create_drop_down)
            Clock.schedule_once(self.create_go_button)
            # self.create_drop_down()
            # self.create_go_button()
    
        def create_drop_down(self, *args):
            for list_name in text_lists:
                # When adding widgets, we need to specify the height manually
                # (disabling the size_hint_y) so the dropdown can calculate
                # the area it needs.
    
                btn = Button(text= list_name, size_hint_y=None, height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))
    
                # for each button, attach a callback that will call the select() method
                # on the dropdown. We'll pass the text of the button as the data of the
                # selection.
                btn.bind(on_release=lambda btn: self.dropdown.select(btn.text),
                         on_press=lambda btn: self.select_list(btn.text))
    
                # then add the button inside the dropdown
                self.dropdown.add_widget(btn)
    
            # create a big main button
            # self.main_button = Button(text='Choose A List', size_hint=(None, None), height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))
            self.main_button = Button(text='Choose A List', background_color=(41/255, 21/255, 228/255, 1))
    
            # show the dropdown menu when the main button is released
            # note: all the bind() calls pass the instance of the caller (here, the
            # mainbutton instance) as the first argument of the callback (here,
            # dropdown.open.).
            self.main_button.bind(on_release=self.dropdown.open)
    
            # one last thing, listen for the selection in the dropdown list and
            # assign the data to the button text.
            self.dropdown.bind(on_select=lambda instance, x: setattr(self.main_button, 'text', x))
            self.top_layout.add_widget(self.main_button)
    
        def create_go_button(self, *args):
            # go_btn = Button(text="Go!", size_hint=(None, None), height=88, width=400, background_color=(41/255, 21/255, 228/255, 1))
            go_btn = Button(text="Go!", background_color=(41/255, 21/255, 228/255, 1))
            self.top_layout.add_widget(go_btn)
    
        def select_list(self, selected):
            self.selected_list = selected
    
    class MyTest(App):
        def build(self):
            return kv
    

    I have modified the code to call the create methods using Clock.schedule_once(), so that it happens after the kv rule is applied. I have also removed the size_hint and size arguments to the Buttons created to allow the GridLayout to size them. I have also commented out some unnecessary code.