【问题标题】:Kivy : How to select item in RecycleView using `up` and `down` key?Kivy:如何使用“向上”和“向下”键在 RecycleView 中选择项目?
【发布时间】:2018-04-12 07:16:10
【问题描述】:

如何在RecycleView 中使用up 和down 键盘键选择项目?

test.py

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.core.window import Window

Builder.load_string('''
<RV>:
    viewclass: 'Label'
    RecycleBoxLayout:
        default_size: None, dp(56)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
''')

class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.data = [{'text': str(x)} for x in range(10)]

    def _keyboard_closed(self):
        pass

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        if keycode[1] == 'down':
            print('down')
        elif keycode[1] == 'up':
            print("up")


class TestApp(App):
    def build(self):
        return RV()

if __name__ == '__main__':
    TestApp().run()

【问题讨论】:

标签: python-2.7 kivy


【解决方案1】:

您需要创建 SelectableLabel 类并更新当前标签

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.core.window import Window

Builder.load_string('''
<RV>:
    viewclass: 'SelectableLabel'
    RecycleBoxLayout:
        default_size: None, dp(56)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
        multiselect: False

<SelectableLabel>:
    # Draw a background to indicate selection
    canvas.before:
        Color:
            rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
        Rectangle:
            pos: self.pos
            size: self.size
''')

class SelectableLabel(RecycleDataViewBehavior, Label):
    ''' Add selection support to the Label '''
    index = None
    selected = BooleanProperty(False)
    selectable = BooleanProperty(True)

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        return super(SelectableLabel, self).refresh_view_attrs(
            rv, index, data)


    def apply_selection(self, rv, index, is_selected):
        ''' Respond to the selection of items in the view. '''
        self.selected = is_selected
        if is_selected:
            print("selection changed to {0}".format(rv.data[index]))
        else:
            print("selection removed for {0}".format(rv.data[index]))

class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.data = [{'text': str(x)} for x in range(10)]
        self.selectedItem = -1

    def _keyboard_closed(self):
        pass

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        print()
        if keycode[1] == 'down':
            self.clearAll()
            self.nextItem()
            print('down')
        elif keycode[1] == 'up':
            self.clearAll()
            self.prevItem()
            print("up")

    def clearAll(self):
        if (self.selectedItem > -1):
            for i in range(len(self.view_adapter.views) - 1):
                self.view_adapter.views[self.selectedItem].selected = 0


    def nextItem(self):
        if self.selectedItem < len(self.data) - 1:
            self.selectedItem += 1
        else:
            self.selectedItem = 0
        self.view_adapter.views[self.selectedItem].selected = 1
        print(self.selectedItem)


    def prevItem(self):     
        if self.selectedItem > 0:
            self.selectedItem -= 1
        else:
            self.selectedItem = len(self.data) - 1
        self.view_adapter.views[self.selectedItem].selected = 1
        print(self.selectedItem)


class TestApp(App):
    def build(self):
        return RV()

if __name__ == '__main__':
    TestApp().run()

【讨论】:

  • 你好@tomasantunes。只是为了通知,如果您增加范围(10)以通过屏幕限制,当您向下移动光标时,您的代码会返回错误:` Traceback(最近一次调用最后一次):` ` 文件“test.py”,第 87 行, 在 nextItem` ` self.view_adapter.views[self.selectedItem].selected = 1` ` KeyError: 11`
  • 此代码失败并出现错误 kivy.factory.FactoryException: Unknown class
【解决方案2】:

我在SelectableRecycleGridLayout 类中实现了一些方法,以利用LayoutSelectionBehavior 类中的方法。有关详细信息,请参阅测试计划、关注点、示例和输出。

测试计划

  1. 单击测试,显示 RecycleView 并突出显示第一行。
  2. 按向下箭头键,下一行突出显示。
  3. 在最后一行,按向下箭头键,第一行高亮显示。
  4. 按向上箭头键,上一行高亮显示。
  5. 在第一行,按向上箭头键,最后一行突出显示。
  6. 按Ctrl + e,弹出窗口显示高亮行的信息。
  7. 鼠标单击任何未突出显示的行,鼠标单击的行被选中,弹出窗口打开。
  8. Popup 关闭后,键盘事件仍然有效!

示例

main.py

​​>
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty, NumericProperty, DictProperty
from kivy.lang import Builder

from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.button import Button
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.popup import Popup
from kivy.core.window import Window
from kivy.clock import Clock

Window.size = (600, 325)


class EditStatePopup(Popup):
    col_data = ListProperty(["?", "?"])

    def __init__(self, obj, **kwargs):
        super(EditStatePopup, self).__init__(**kwargs)
        self.col_data[0] = obj["Id"]
        self.col_data[1] = obj["Name"]


class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
                                  RecycleGridLayout):
    ''' Adds selection and focus behaviour to the view. '''

    selected_row = NumericProperty(0)

    def get_nodes(self):
        nodes = self.get_selectable_nodes()
        if self.nodes_order_reversed:
            nodes = nodes[::-1]
        if not nodes:
            return None, None

        selected = self.selected_nodes
        if not selected:    # nothing selected, select the first
            self.select_node(nodes[0])
            self.selected_row = 0
            return None, None

        if len(nodes) == 1:     # the only selectable node is selected already
            return None, None

        last = nodes.index(selected[-1])
        self.clear_selection()
        return last, nodes

    def select_next(self):
        ''' Select next row '''
        last, nodes = self.get_nodes()
        if not nodes:
            return

        if last == len(nodes) - 1:
            self.select_node(nodes[0])
            self.selected_row = nodes[0]
        else:
            self.select_node(nodes[last + 1])
            self.selected_row = nodes[last + 1]

    def select_previous(self):
        ''' Select previous row '''
        last, nodes = self.get_nodes()
        if not nodes:
            return

        if not last:
            self.select_node(nodes[-1])
            self.selected_row = nodes[-1]
        else:
            self.select_node(nodes[last - 1])
            self.selected_row = nodes[last - 1]

    def select_current(self):
        ''' Select current row '''
        last, nodes = self.get_nodes()
        if not nodes:
            return

        self.select_node(nodes[self.selected_row])


class SelectableButton(RecycleDataViewBehavior, Button):
    ''' Add selection support to the Button '''
    index = None
    selected = BooleanProperty(False)
    selectable = BooleanProperty(True)

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''

        self.index = index
        return super(SelectableButton, self).refresh_view_attrs(rv, index, data)

    def on_touch_down(self, touch):
        ''' Add selection on touch down '''
        if super(SelectableButton, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos) and self.selectable:
            print("on_touch_down: self=", self)
            return self.parent.select_with_touch(self.index, touch)

    def apply_selection(self, rv, index, is_selected):
        ''' Respond to the selection of items in the view. '''
        self.selected = is_selected


class RV(BoxLayout):
    data_items = ListProperty([])
    row_data = DictProperty({})
    col1_data = ListProperty([])
    col2_data = ListProperty([])
    col1_row_controller = ObjectProperty(None)
    col2_row_controller = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.get_states()
        Clock.schedule_once(self.set_default_first_row, .0005)
        self._request_keyboard()

    def _request_keyboard(self):
        self._keyboard = Window.request_keyboard(
            self._keyboard_closed, self, 'text'
        )
        if self._keyboard.widget:
            # If it exists, this widget is a VKeyboard object which you can use
            # to change the keyboard layout.
            pass
        self._keyboard.bind(on_key_down=self._on_keyboard_down)

    def _keyboard_closed(self):
        self._keyboard.unbind(on_key_down=self._on_keyboard_down)
        self._keyboard = None

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        if keycode[1] == 'down':    # keycode[274, 'down'] pressed
            # Respond to keyboard down arrow pressed
            self.display_keystrokes(keyboard, keycode, text, modifiers)
            self.col1_row_controller.select_next()
            self.col2_row_controller.select_next()

        elif keycode[1] == 'up':    # keycode[273, 'up] pressed
            # Respond to keyboard up arrow pressed
            self.display_keystrokes(keyboard, keycode, text, modifiers)
            self.col1_row_controller.select_previous()
            self.col2_row_controller.select_previous()

        elif len(modifiers) > 0 and modifiers[0] == 'ctrl' and text == 'e':     # ctrl + e pressed
            # Respond to keyboard ctrl + e pressed, and call Popup
            self.display_keystrokes(keyboard, keycode, text, modifiers)
            keyboard.release()
            self.on_keyboard_select()

        # Keycode is composed of an integer + a string
        # If we hit escape, release the keyboard
        if keycode[1] == 'escape':
            keyboard.release()

        # Return True to accept the key. Otherwise, it will be used by
        # the system.
        return True

    def display_keystrokes(self, keyboard, keycode, text, modifiers):
        print("\nThe key", keycode, "have been pressed")
        print(" - text is %r" % text)
        print(" - modifiers are %r" % modifiers)

    def on_keyboard_select(self):
        ''' Respond to keyboard event to call Popup '''

        # setup row data for Popup
        self.row_data = self.col1_data[self.col1_row_controller.selected_row]

        # call Popup
        self.popup_callback()

    def on_mouse_select(self, instance):
        ''' Respond to mouse event to call Popup '''

        if (self.col1_row_controller.selected_row != instance.index
                or self.col2_row_controller.selected_row != instance.index):
            # Mouse clicked on row is not equal to current selected row
            self.col1_row_controller.selected_row = instance.index
            self.col2_row_controller.selected_row = instance.index

            # Hightlight mouse clicked/selected row
            self.col1_row_controller.select_current()
            self.col2_row_controller.select_current()

        # setup row data for Popup
        # we can use either col1_data or col2_data because they are duplicate and each stores the same info
        self.row_data = self.col1_data[instance.index]

        # call Popup
        self.popup_callback()

    def popup_callback(self):
        ''' Instantiate and Open Popup '''
        popup = EditStatePopup(self.row_data)
        popup.open()

        # enable keyboard request
        self._request_keyboard()

    def set_default_first_row(self, dt):
        ''' Set default first row as selected '''
        self.col1_row_controller.select_next()
        self.col2_row_controller.select_next()

    def update(self):
        self.col1_data = [{'text': str(x[0]), 'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'selectable': True}
                          for x in self.data_items]

        self.col2_data = [{'text': x[1], 'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'selectable': True}
                          for x in self.data_items]

    def get_states(self):
        rows = [(1, 'Test1'), (2, 'Test2'), (3, 'Test3')]

        i = 0
        for row in rows:
            self.data_items.append([row[0], row[1], i])
            i += 1
        print(self.data_items)
        self.update()


class MainMenu(BoxLayout):
    states_cities_or_areas = ObjectProperty(None)
    rv = ObjectProperty(None)

    def display_states(self):
        self.remove_widgets()
        self.rv = RV()
        self.states_cities_or_areas.add_widget(self.rv)

    def remove_widgets(self):
        self.states_cities_or_areas.clear_widgets()


class FactApp(App):
    title = "test"

    def build(self):
        self.root = Builder.load_file('test.kv')
        return MainMenu()


if __name__ == '__main__':
    FactApp().run()

test.kv

#:kivy 1.10.0

<EditStatePopup>:
    size_hint: None, None
    title_size: 20
    title_font: "Verdana"
    size: 400, 275
    auto_dismiss: False

    BoxLayout:
        orientation: "vertical"
        GridLayout:
            cols: 2
            #background_color: 0, 0.517, 0.705, 1
            spacing: 10, 10
            padding: 20, 20
            Label:
                text: "Id"
                text_size: self.size
            Label:
                text: root.col_data[0]
                text_size: self.size
            Label:
                text: "Name"
                text_size: self.size
                valign: 'middle'
            TextInput:
                text: root.col_data[1]
                text_size: self.size
                focus: True
        BoxLayout:
            Button:
                size_hint: 1, 0.4
                text: "Save Changes"
                on_release:
                    root.dismiss()
            Button:
                size_hint: 1, 0.4
                text: "Cancel Changes"
                on_release: root.dismiss()


<SelectableButton>:
    canvas.before:
        Color:
            rgba: (0, 0.517, 0.705, 1) if self.selected else (0, 0.517, 0.705, 1)
        Rectangle:
            pos: self.pos
            size: self.size
    background_color: [1, 0, 0, 1]  if self.selected else [1, 1, 1, 1]  # dark red else dark grey
    on_press: app.root.rv.on_mouse_select(self)


<RV>:
    col1_row_controller: col1_row_controller
    col2_row_controller: col2_row_controller

    orientation: "vertical"

    GridLayout:
        size_hint: 1, None
        size_hint_y: None
        height: 25
        cols: 3

        Label:
            size_hint_x: .1
            text: "Id"
        Label:
            size_hint_x: .5
            text: "Name"

    BoxLayout:
        RecycleView:
            size_hint_x: .1
            data: root.col1_data
            viewclass: 'SelectableButton'
            SelectableRecycleGridLayout:
                id: col1_row_controller
                key_selection: 'selectable'
                cols: 1
                default_size: None, dp(26)
                default_size_hint: 1, None
                size_hint_y: None
                height: self.minimum_height
                orientation: 'vertical'
                multiselect: True
                touch_multiselect: True

        RecycleView:
            size_hint_x: .5
            data: root.col2_data
            viewclass: 'SelectableButton'
            SelectableRecycleGridLayout:
                id: col2_row_controller
                key_selection: 'selectable'
                cols: 1
                default_size: None, dp(26)
                default_size_hint: 1, None
                size_hint_y: None
                height: self.minimum_height
                orientation: 'vertical'
                multiselect: True
                touch_multiselect: True


<MenuButton@Button>:
    text_size: self.size
    valign: "middle"
    padding_x: 5
    size : (80,30)
    size_hint : (None, None)
    background_color: 90 , 90, 90, 90
    background_normal: ''
    color: 0, 0.517, 0.705, 1
    border: (0, 10, 0, 0)


<MainMenu>:
    states_cities_or_areas: states_cities_or_areas

    BoxLayout:
        orientation: 'vertical'
        #spacing : 10

        BoxLayout:
            canvas.before:
                Rectangle:
                    pos: self.pos
                    size: self.size

            size_hint_y: 1

            MenuButton:
                id: btn
                text: 'Test'
                size : (60,30)
                on_release: root.display_states()


        BoxLayout:
            canvas.before:
                Rectangle:
                    pos: self.pos
                    size: self.size

                Color:
                    rgb: (1,1,1)

            Label:
                size_hint_x: 45

        BoxLayout:
            id: states_cities_or_areas
            size_hint_y: 10

        Label:
            size_hint_y: 1

输出

【讨论】:

    【解决方案3】:

    Based on tomasantunes' solution, but contains auto-scrolling and the bug is fixed (when Kivy crashes when an item out of the box is selected)

    from kivy.app import App
    from kivy.core.window import Window
    from kivy.lang import Builder
    from kivy.uix.screenmanager import ScreenManager, Screen
    from kivy.uix.button import Button
    from kivy.uix.floatlayout import FloatLayout
    from kivy.uix.boxlayout import BoxLayout
    from kivy.uix.label import Label
    from kivy.uix.recycleview import RecycleView
    from kivy.uix.image import Image
    from kivy.graphics import Color, Rectangle
    from kivy.animation import Animation
    from kivy.clock import Clock
    from kivy.properties import BooleanProperty
    from kivy.uix.recycleview.views import RecycleDataViewBehavior
    from kivy.uix.recycleboxlayout import RecycleBoxLayout
    from kivy.uix.behaviors import FocusBehavior
    from kivy.uix.recycleview.layout import LayoutSelectionBehavior
    
    import subprocess
    import time
    
    
    Builder.load_string(
        """
    <SelectableLabel>:
        # Draw a background to indicate selection
        color: (0.0, 0.0, 0.0, 0.9) if self.selected else (1.0, 1.0, 1.0, 1.0)
        canvas.before:
            Color:
                rgba: (0.9, 0.6, 0.0, 1.0) if self.selected else (0.0, 0.0, 0.0, 1.0)
            Rectangle:
                pos: self.pos
                size: self.size
    
    <RV>:
        viewclass: 'SelectableLabel'
        scroll_y: 0
        effect_cls: "ScrollEffect" 
        SelectableRecycleBoxLayout:
            id: box
            default_size: 360, dp(56)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            multiselect: False
            touch_multiselect: False
    """
    )
    
    
    class SelectableRecycleBoxLayout(
        FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout
    ):
        """ Adds selection and focus behaviour to the view. """
    
    
    class SelectableLabel(RecycleDataViewBehavior, Label):
        """ Add selection support to the Label """
    
        index = None
        selected = BooleanProperty(False)
        selectable = BooleanProperty(True)
    
        def refresh_view_attrs(self, rv, index, data):
            """ Catch and handle the view changes """
            self.index = index
            return super(SelectableLabel, self).refresh_view_attrs(rv, index, data)
    
        def apply_selection(self, rv, index, is_selected):
            """ Respond to the selection of items in the view. """
            self.selected = is_selected
            if is_selected:
                print("selection changed to {0}".format(rv.data[index]))
            else:
                print("selection removed for {0}".format(rv.data[index]))
    
    
    class RV(RecycleView):
        def __init__(self, **kwargs):
            super(RV, self).__init__(**kwargs)
            cells = list(Cell.all('wlan0'))
    
            self.data = [
                {
                    "text": "Item " + str(x),
                    "markup": "True",
                    "font_size": "30px",
                }
                for x in range(100)
            ]
            self.selectedItem = -1
            self.scroll_y = 1
    
        def nextItem(self):
            if self.selectedItem < len(self.data) - 1:
                self.selectedItem += 1
            else:
                return
      
            self.scroll_to_index(self.selectedItem)
            self.ids.box.select_node(self.selectedItem)
            print(self.selectedItem)
    
        def prevItem(self):
            if self.selectedItem > 0:
                self.selectedItem -= 1
            else:
                return
            
            self.scroll_to_index(self.selectedItem)
            self.ids.box.select_node(self.selectedItem)
            print(self.selectedItem)
    
        def selectItem(self, index):
            self.selectedItem = index
            self.scroll_to_index(self.selectedItem)
            self.ids.box.select_node(index)
    
        def scroll_to_index(self, index):
            box = self.children[0]
            pos_index = (box.default_size[1] + box.spacing) * index
            scroll = self.convert_distance_to_scroll(
                0, pos_index - (self.height * 0.5))[1]
            if scroll > 1.0:
                scroll = 1.0
            elif scroll < 0.0:
                scroll = 0.0
            self.scroll_y = 1.0 - scroll
    
    class ConnectScreen(Screen):
        def __init__(self, **kwargs):
            super(ConnectScreen, self).__init__(**kwargs)
            layout = FloatLayout()
            
            self.wifiList = RV()
    
            layout.add_widget(self.wifiList)
            self.add_widget(layout)
            return None
    
        def nextItem(self):
            self.wifiList.nextItem()
    
        def prevItem(self):
            self.wifiList.prevItem()
    
        def selectItem(self, index):
            self.wifiList.selectItem(index)
    
    
    connectScreen = ConnectScreen(name="connect")
    
    sm = ScreenManager()
    sm.add_widget(connectScreen)
    
    class PasswordApp(App):
        # Build
        def build(self):
            self._keyboard = Window.request_keyboard(
                self._keyboard_closed, self.root, "text"
            )
            self._keyboard.bind(on_key_down=self._on_keyboard_down)
    
            # Window.show_cursor = False
    
            return sm
    
        # Keyboard Down Event
        def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
            if keycode[1] == "f10":
                App.get_running_app().stop()
            elif keycode[1] == "down":
                connectScreen.nextItem()
            elif keycode[1] == "up":
                connectScreen.prevItem()
    
        # Keyboard Closed Event
        def _keyboard_closed(self):
            elf._keyboard.unbind(on_key_down=self._on_keyboard_down)
            self._keyboard = None
    
    PasswordApp().run()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多