KivyMD RecycleView 网格中的复选框操作重复

CheckBox Action Repeats in a KivyMD RecycleView Grid

当项目的复选框在回收视图网格中为 clicked/unclicked 时,click/unclick 也会自动重复网格中的其他数据项。为什么会这样?下面的代码是一个最小的工作示例。谢谢

from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty
from kivy.clock import Clock

from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox

Builder.load_string("""
<Check>:

<GridTile>:
    SmartTile:
        source: root.tile
        size_hint_y: None
        height: '150dp'
        Check:

<GridScreen>:
    name: 'grid_screen'
    RV:
        id: rv
        viewclass: 'GridTile'
        RecycleGridLayout:
            cols: 2
            size_hint_y: None
            default_size: 1, dp(150)
            default_size_hint: 1, None
            height: self.minimum_height
""")

class GridTile(Screen):
    tile = StringProperty('')

class GridScreen(Screen):
    pass

class RV(RecycleView):
    data = ListProperty('[]')

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.cell_data()

    def cell_data(self):
        self.data = [{"tile": 'The Beatles'} for i in range(41)]

class Check(SmartTile):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.add_checkbox)

    def add_checkbox(self, interval):
        app = MDApp.get_running_app()
        self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
        self.check.bind(active=app.on_checkbox_active)
        self._box_overlay.add_widget(self.check)

class ThisApp(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(GridScreen(name='grid_screen'))
        return self.sm

    def on_checkbox_active(self, checkbox, value):
        if value:
            print('The checkbox', checkbox, 'is active', 'and', checkbox.state, 'state')
        else:
            print('The checkbox', checkbox, 'is inactive', 'and', checkbox.state, 'state')

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

RecycleView 通过回收最少数量的 viewclass 实例来工作,在您的情况下是 GridTileRecycleView 根据 data 中的条目将属性分配给 GridTile 的那些实例。如果您更改 GridTile 或其子项的任何属性,而这些属性未在 data 中处理,则 RecycleView 不会意识到这些更改,并且这些更改将保留在 [= 的回收实例中=12=]。因此,如果您希望 MDCheckBox 状态得到正确处理,您必须将其作为 GridTile 的另一个 属性 包含在您的 data 中。事实上,您的 MDCheckBox 不在您的 kv 中,这使得这更难实现。这回答了 why 问题。

这是您原始发布代码的修改版本。此版本有效,但 GridTile 个实例之间存在一些交互(当您单击一个复选框时,另一个 GridTile 似乎会自行刷新)。我只见过这种与 KivyMd 的互动。在没有 KivyMD 的情况下编写类似的应用程序不会显示奇怪的交互。

from functools import partial

from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty, ObjectProperty
from kivy.clock import Clock

from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox

Builder.load_string("""
<GridTile>:
    SmartTile:
        source: root.tile
        size_hint_y: None
        height: '150dp'
        Check:
            id: ck
            root_ref: root  # creat reference to containing GridTile

<GridScreen>:
    name: 'grid_screen'
    RV:
        id: rv
        viewclass: 'GridTile'
        RecycleGridLayout:
            cols: 2
            size_hint_y: None
            default_size: 1, dp(150)
            default_size_hint: 1, None
            height: self.minimum_height
""")

class GridTile(Screen):
    # properties to be set in the rv.data
    tile = StringProperty('')
    index = NumericProperty(-1)
    cb_state = StringProperty('normal')

    def __init__(self, **kwargs):
        super(GridTile, self).__init__(**kwargs)
        self.bind(cb_state=self.set_cb_state)  # bind the cb_state property to set the state of the MDCheckBox

    def set_cb_state(self, gridtile, cb_state_value):
        self.ids.ck.check.state = cb_state_value  # actually set the state of the MDCheckBox

class GridScreen(Screen):
    pass

class RV(RecycleView):
    data = ListProperty('[]')

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.cell_data()

    def cell_data(self):
        self.data = [{"tile": 'The Beatles', "index": i, "cb_state": 'normal'} for i in range(41)]

class Check(SmartTile):
    root_ref = ObjectProperty(None)  # reference to the containing GridTile (set by kv)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.add_checkbox)

    def add_checkbox(self, interval):
        app = MDApp.get_running_app()
        self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
        self.check.bind(on_press=partial(app.on_checkbox_press, self))  # bind to on_press to avoid possible looping when active is changed
        self._box_overlay.add_widget(self.check)

class ThisApp(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(GridScreen(name='grid_screen'))
        return self.sm

    def on_checkbox_press(self, check, checkbox):
        new_state = checkbox.state

        # set checkbox state back to the default
        checkbox.state = 'normal'  # avoids setting checkbox state without data

        rv = self.root.get_screen('grid_screen').ids.rv
        rv.data[check.root_ref.index]['cb_state'] = new_state
        rv.refresh_from_data()  # set the state from data

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

修改的要点是将 indexcb_state 属性添加到 GridTile class 和 dataindex 属性 只是在调整 data 时用作 data 的索引。 cb_stateMDCheckboxstate。由于 MDCheckbox 没有出现在 kv 中,如果 cb_state 属性 到 MDChckbox 的实际 state 则不会自动绑定,以便在 GridTile class 中显式创建绑定。此外,MDCheckbox 更新 data 的绑定更改为绑定到 on_press,而不是 on_active,因为 active 属性将由 RecycleView 基于 data 更改,并可能导致循环效果。