无法从 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。
问题是每次measure
TextInput
获得焦点时,都会在MeasureDropDown
的on_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))
请注意,此答案使用 fbind
和 funbind
(bind
和 unbind
不会像这样工作)。
结合我的第一个答案和代码来处理 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
BooleanProperty
到 MeasureDropDown
class 以跟踪所选小部件是否是 DropListTextInput
。
如果选定的小部件是 DropListTextInput
,新的 handle_focus()
方法不会关闭 MeasureDropDown
。
DropListTextInput
被限制为单行,因此在其中点击 Enter
将触发 on_text_validate()
方法,该方法将文本设置在 measure
TextInput
并关闭 MeasureDropDown
.
我使用 Builder.load_string()
只是为了方便。
在我的 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。
问题是每次measure
TextInput
获得焦点时,都会在MeasureDropDown
的on_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))
请注意,此答案使用 fbind
和 funbind
(bind
和 unbind
不会像这样工作)。
结合我的第一个答案和代码来处理 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
BooleanProperty
到 MeasureDropDown
class 以跟踪所选小部件是否是 DropListTextInput
。
如果选定的小部件是 DropListTextInput
,新的 handle_focus()
方法不会关闭 MeasureDropDown
。
DropListTextInput
被限制为单行,因此在其中点击 Enter
将触发 on_text_validate()
方法,该方法将文本设置在 measure
TextInput
并关闭 MeasureDropDown
.
我使用 Builder.load_string()
只是为了方便。