Cannot get correct item selection from DropDown widget in Kivy

半腔热情 提交于 2021-01-28 05:27:45

问题


In my Kivy app, one of the text inputs triggers the opening of a DropDown widget when on_focus. The textinput is part of a custom BoxLayout IngredientRow which I dinamically add to the screen on the press of a button.

What I want is to fill the textinput with the text of the button selected from the DropDown. This works for the first IngredientRow. However, when I add new rows, selecting an item from the DropDown in a row different from the first, will fill the textinput from the first row. See below a minimal working example:

The py file:

from kivy.app import App
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.textinput import TextInput


class DelIngButton(Button):
    pass
class DropListButton(Button):
    def __init__(self, **kwargs):
        super(DropListButton, self).__init__(**kwargs)
        self.bind(on_release=lambda x: self.parent.parent.select(self.text))
class IngredientRow(BoxLayout):
    pass
class MeasureDropDown(DropDown):
    pass

####################################
class AddWindow(Screen):
    def __init__(self, **kwargs):
        super(AddWindow, self).__init__(**kwargs)

        self.DropDown = MeasureDropDown()

    def addIngredient(self, instance): #adds a new IngredientRow
        row = instance.parent
        row.remove_widget(row.children[0])
        row.add_widget(Factory.DelIngButton(), index=0)
        self.ingsGrid.add_widget(Factory.IngredientRow(), index=0)


class WMan(ScreenManager):
    def __init__(self, **kwargs):
        super(WMan, self).__init__(**kwargs)

kv = Builder.load_file("ui/layout.kv")

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

if __name__ == "__main__":
    RecipApp().run()

and the kv file:

#:set text_color 0,0,0,.8

#:set row_height '35sp'

#:set main_padding ['10sp', '10sp']
#:set small_padding ['5sp', '5sp']


<DropListButton>: # Button for custom DropDown
    color: text_color
    background_normal: ''

<DelIngButton>: # Button to delete row
    text: '-'
    size_hint: None, None
    height: row_height
    width: row_height
    on_release: self.parent.parent.remove_widget(self.parent)

<MeasureDropDown>:
    id: dropDown
    DropListButton:
        size_hint: 1, None
        height: row_height
        text: "g"
    DropListButton:
        size_hint: 1, None
        height: row_height
        text: "Kg"
    TextInput:
        size_hint: 1, None
        height: row_height
        hint_text: 'new'

<IngredientRow>:
    orientation: 'horizontal'
    size_hint: 1, None
    height: row_height
    spacing: '5sp'
    TextInput:
        id: ing
        hint_text: 'Ingredient'
        multiline: False
        size_hint: .6, None
        height: row_height
    TextInput:
        id: quant
        hint_text: 'Quantity'
        multiline: False
        size_hint: .2, None
        height: row_height
    TextInput:
        id: measure
        hint_text: 'measure'
        size_hint: .2, None
        height: row_height
        on_focus:
            app.root.ids.add.DropDown.open(self) if self.focus else app.root.ids.add.DropDown.dismiss(self)
            app.root.ids.add.DropDown.bind(on_select=lambda self, x: setattr(app.root.ids.add.ingredientRow.children[1], 'text', x))
    Button:
        id: addIng
        text: "+"
        size_hint: None, None
        height: row_height
        width: row_height
        on_release: app.root.ids.add.addIngredient(self)


<MainScrollView@ScrollView>:
    size_hint: 1, None
    scroll_type: ['bars', 'content']

##################
# Windows
##################

WMan:
    AddWindow:
        id: add

<AddWindow>:
    name: 'add'
    ingsGrid: ingsGrid
    ingredientRow: ingredientRow

    MainScrollView:
        height: self.parent.size[1]
        GridLayout:
            cols:1
            size_hint: 1, None
            pos_hint: {"top": 1}
            height: self.minimum_height
            padding: main_padding
            StackLayout:
                id: ingsGrid
                size_hint: 1, None
                height: self.minimum_height
                orientation: 'lr-tb'
                padding: small_padding
                IngredientRow:
                    id: ingredientRow

I understand the problem is with the following part of the code:

on_select=lambda self, x: setattr(app.root.ids.add.ingredientRow.children[1], 'text', x)

as this will always call the first IngredientRow. However, I could not figure out how to refer to the IngredientRow where the DropDown is called.


回答1:


Combining my first answer with code to handle the TextInput in the MeasureDropDown:

from kivy.app import App
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.properties import BooleanProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.textinput import TextInput


class DelIngButton(Button):
    pass


class DropListButton(Button):
    def __init__(self, **kwargs):
        super(DropListButton, self).__init__(**kwargs)
        self.bind(on_release=lambda x: self.parent.parent.select(self.text))


class DropListTextInput(TextInput):
    # Provides a couple needed behaviors

    def on_focus(self, *args):
        if self.focus:
            self.dropDown.selection_is_DLTI = True
        else:
            self.dropDown.selection_is_DLTI = False

    def on_text_validate(self, *args):
        self.dropDown.selection_is_DLTI = False

        # put the text from this widget into the TextInput that the DropDown is attached to
        self.dropDown.attach_to.text = self.text

        # dismiss the DropDown
        self.dropDown.dismiss()


class IngredientRow(BoxLayout):
    def __init__(self, **kwargs):
        super(IngredientRow, self).__init__(**kwargs)
        self.dropdown = MeasureDropDown()

    def handle_focus(self, ti):
        # handle on_focus event for the measure TextInput
        if ti.focus:
            # open DropDown if the TextInput gets focus
            self.dropdown.open(ti)
        else:
            # ti has lost focus
            if self.dropdown.selection_is_DLTI:
                # do not dismiss if a DropListTextInput is the selection
                return

            # dismiss DropDown
            self.dropdown.dismiss(ti)
            self.dropdown.unbind_all()
            self.dropdown.fbind('on_select', lambda self, x: setattr(ti, 'text', x))


class MeasureDropDown(DropDown):
    # set to True if the selection is a DropListTextInput
    selection_is_DLTI = BooleanProperty(False)

    def unbind_all(self):
        for callBack in self.get_property_observers('on_select'):
            self.funbind('on_select', callBack)


####################################
class AddWindow(Screen):

    def addIngredient(self, instance): #adds a new IngredientRow
        row = instance.parent
        row.remove_widget(row.children[0])
        row.add_widget(Factory.DelIngButton(), index=0)
        self.ingsGrid.add_widget(Factory.IngredientRow(), index=0)


class WMan(ScreenManager):
    def __init__(self, **kwargs):
        super(WMan, self).__init__(**kwargs)

# kv = Builder.load_file("ui/layout.kv")
kv = Builder.load_string('''
#:set text_color 0,0,0,.8

#:set row_height '35sp'

#:set main_padding ['10sp', '10sp']
#:set small_padding ['5sp', '5sp']


<DropListButton>: # Button for custom DropDown
    color: text_color
    background_normal: ''

<DelIngButton>: # Button to delete row
    text: '-'
    size_hint: None, None
    height: row_height
    width: row_height
    on_release: self.parent.parent.remove_widget(self.parent)

<MeasureDropDown>:
    id: dropDown
    DropListButton:
        size_hint: 1, None
        height: row_height
        text: "g"
    DropListButton:
        size_hint: 1, None
        height: row_height
        text: "Kg"
    DropListTextInput:  # CustomTextInput instead of standard TextInput
        dropDown: dropDown  # provide easy access to the DropDown
        size_hint: 1, None
        height: row_height
        hint_text: 'new'
        multiline: False  # needed to trigger on_text_validate

<IngredientRow>:
    orientation: 'horizontal'
    size_hint: 1, None
    height: row_height
    spacing: '5sp'
    TextInput:
        id: ing
        hint_text: 'Ingredient'
        multiline: False
        size_hint: .6, None
        height: row_height
    TextInput:
        id: quant
        hint_text: 'Quantity'
        multiline: False
        size_hint: .2, None
        height: row_height
    TextInput:
        id: measure
        hint_text: 'measure'
        size_hint: .2, None
        height: row_height
        on_focus:
            root.handle_focus(self)  # focus event is now handled in the IngredientRow class
    Button:
        id: addIng
        text: "+"
        size_hint: None, None
        height: row_height
        width: row_height
        on_release: app.root.ids.add.addIngredient(self)


<MainScrollView@ScrollView>:
    size_hint: 1, None
    scroll_type: ['bars', 'content']

##################
# Windows
##################

WMan:
    AddWindow:
        id: add

<AddWindow>:
    name: 'add'
    ingsGrid: ingsGrid
    ingredientRow: ingredientRow

    MainScrollView:
        height: self.parent.size[1]
        GridLayout:
            cols:1
            size_hint: 1, None
            pos_hint: {"top": 1}
            height: self.minimum_height
            padding: main_padding
            StackLayout:
                id: ingsGrid
                size_hint: 1, None
                height: self.minimum_height
                orientation: 'lr-tb'
                padding: small_padding
                IngredientRow:
                    id: ingredientRow
''')


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


if __name__ == "__main__":
    RecipApp().run()

I have added a DropListTextInput class for use in the MeasureDropDown and added a handle_focus() method to the IngredientRow class.

I have also added a selection_is_DLTI BooleanProperty to the MeasureDropDown class which keeps track of whether the selected widget is a DropListTextInput.

The new handle_focus() method does not dismiss the MeasureDropDown if the selected widget is a DropListTextInput.

The DropListTextInput is limited to a single line, so that hitting Enter in it will trigger the on_text_validate() method, which sets the text in the measure TextInput and dismisses the MeasureDropDown.

I used Builder.load_string() just for my own convenience.




回答2:


The problem is that every time a measure TextInput gets focus, another lambda function is added to the on_select event of MeasureDropDown, and none are ever unbound. That means that every time one of the dropdown choices gets selected, all of those accumulated lambda functions are executed, so each TextInput that has ever gotten focus gets its text changed.

One way to fix that would be to create a separate MeasureDropDown for each IngredientRow.

Another approach is to unbind all the prior lambda functions before binding the current one. Here are some changes to your code that accomplish that:

class MeasureDropDown(DropDown):
    def unbind_all(self):
        # unbind all the current call backs for `on_slect`
        for callBack in self.get_property_observers('on_select'):
            self.funbind('on_select', callBack)

Then use the unbind_all() method in the kv:

TextInput:
    id: measure
    hint_text: 'measure'
    size_hint: .2, None
    height: row_height
    on_focus:
        app.root.ids.add.DropDown.open(self) if self.focus else app.root.ids.add.DropDown.dismiss(self)
        app.root.ids.add.DropDown.unbind_all()
        app.root.ids.add.DropDown.fbind('on_select', lambda self, x: setattr(root.ids.measure, 'text', x))

Note this answer uses fbind and funbind (bind and unbind won't work like this).



来源:https://stackoverflow.com/questions/62304189/cannot-get-correct-item-selection-from-dropdown-widget-in-kivy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!