无法从 Kivy 中的 DropDown 小部件获得正确的项目选择

Cannot get correct item selection from DropDown widget in Kivy

在我的 Kivy 应用程序中,其中一个文本输入会在 on_focus 时触发 DropDown 小部件的打开。 textinput 是自定义 BoxLayout IngredientRow 的一部分,我通过按下按钮将其动态添加到屏幕。

我想要的是用从 DropDown 中选择的按钮的文本填充文本输入。这适用于第一个 IngredientRow。但是,当我添加新行时,从不同于第一行的 DropDown 中选择一个项目,将填充第一行的文本输入。请参阅下面的最小工作示例:

py文件:

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()

和 kv 文件:

#: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

我知道问题出在代码的以下部分:

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

因为这将始终调用第一个 IngredientRow。但是,我不知道如何引用调用 DropDown 的 IngredientRow。

问题是每次measureTextInput获得焦点时,都会在MeasureDropDownon_select事件中添加另一个lambda函数,none 永远不受约束。这意味着每次选择一个下拉选项时,都会执行所有这些累积的 lambda 函数,因此每个获得焦点的 TextInput 都会更改其文本。

解决此问题的一种方法是为每个 IngredientRow.

创建一个单独的 MeasureDropDown

另一种方法是在绑定当前函数之前取消绑定所有先前的 lambda 函数。以下是对您的代码进行的一些更改以实现此目的:

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)

然后在kv中使用unbind_all()方法:

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))

请注意,此答案使用 fbindfunbindbindunbind 不会像这样工作)。

结合我的第一个答案和代码来处理 MeasureDropDown 中的 TextInput:

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()

我在 MeasureDropDown 添加了一个 DropListTextInput class 并在 IngredientRow class 添加了一个 handle_focus() 方法。

我还添加了一个 selection_is_DLTI BooleanPropertyMeasureDropDown class 以跟踪所选小部件是否是 DropListTextInput

如果选定的小部件是 DropListTextInput,新的 handle_focus() 方法不会关闭 MeasureDropDown

DropListTextInput 被限制为单行,因此在其中点击 Enter 将触发 on_text_validate() 方法,该方法将文本设置在 measure TextInput 并关闭 MeasureDropDown.

我使用 Builder.load_string() 只是为了方便。