Python/Kivy:使用“向上"和“向下"键在 RecycleView 中选择行 [英] Python/Kivy : Selection Row in RecycleView using `Up` and `Down` key

查看:26
本文介绍了Python/Kivy:使用“向上"和“向下"键在 RecycleView 中选择行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 python-2.7 和 kivy.当我运行 test.py 并单击 Test 菜单时,屏幕显示如附加图像.
1.如何在屏幕加载时默认highlight第一行?当按键盘的updown键时,应该根据updown键选择行.
2. 当我点击任何一行时,它会在 modify case 中打开.如何使用 ctrl+e 而不是在 modifycase 中打开选定的行点击?

I am using python-2.7 and kivy. When i run test.py and click on Test menu then screen shows like attached image.
1. How to highlight first row by default on screen load? When press up and down key of keyboard then it should be select row according up and down key .
2. When i click on any row then it opens in modify case.How to open selected row in modifycase using ctrl+e instead of click?

import kivy

kivy.require('1.9.0')
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty,NumericProperty
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView

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
Window.size = (600, 325)
from kivy.clock import Clock


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

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



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


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

    def __init__(self, **kwargs):
        super(SelectableButton, self).__init__(**kwargs)
        Clock.schedule_interval(self.update, .0005)



    def update(self, *args):
        self.text = self.rv_data[self.index][self.key]

    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:
            return self.parent.select_with_touch(self.index, touch)

    def apply_selection(self, rv, index, is_selected):
        self.selected = is_selected
        self.rv_data = rv.data



    def on_press(self):
        popup = EditStatePopup(self)
        popup.open()

class MyRV(RecycleView):
    def __init__(self, **kwargs):
        super(MyRV, self).__init__(**kwargs)
        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.selectedItem = -1

    def _keyboard_closed(self):
        pass

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        if keycode[1] == 'down':
            self.clearAll()
            self.nextItem()
            print('down')
        elif keycode[1] == 'up':
            self.clearAll()
            self.prevItem()
            print("up")
        elif keycode[1] == 'e' and len(modifiers) > 0 and modifiers[0] == 'ctrl':
            self.view_adapter.views[self.selectedItem].on_press()

    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.view_adapter.views) - 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.view_adapter.views) - 1
        self.view_adapter.views[self.selectedItem].selected = 1
        print(self.selectedItem)



class RV(RecycleView):
    data_items = ListProperty([])
    col1 = ListProperty()
    col2 = ListProperty()

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

    def update(self):
        self.col1 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Id', 'text': str(x[2])} for x in self.data_items]
        self.col2 = [{'Id': str(x[0]), 'Name': x[1], 'key': 'Name', 'text': str(x[2])} 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()

    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 TestApp(App):
    title = "test"

    def build(self):
        return MainMenu()



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

test.kv

<SelectableButton>:
    canvas.before:
        Color:
            rgba: (10, 10, 10, 10) if self.selected else (0, 0.517, 0.705, 1)
        Rectangle:
            pos: self.pos
            size: self.size


<MyRV@RecycleView>:
    viewclass: 'SelectableButton'
    SelectableRecycleGridLayout:
        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

<RV>:
    BoxLayout:
        orientation: "vertical"
        viewclass: 'SelectableButton'
        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:
            MyRV:
                size_hint_x: .1
                data: root.col1
            MyRV:
                size_hint_x: .5
                data: root.col2



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

    BoxLayout:
        orientation: "vertical"
        GridLayout:
            cols: 2
            #backgroun_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:
                focus : True
                text: root.col_data[1]
                text_size: self.size

        GridLayout:
            cols: 2
            padding: 10, 0, 10, 10
            spacing: 10, 10
            row_default_height: '20dp'
            size_hint: .5, .2
            pos_hint: {'x': .25, 'y':.65}

            Button:
                text: 'Ok'


            Button:
                text: 'Cancel'
                size_hint_x: .5
                on_release: root.dismiss()




<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

推荐答案

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

I have implemented a few methods in the class SelectableRecycleGridLayout to make use of methods in the class LayoutSelectionBehavior. Please refer to the test plans, concerns, example, and output for details.

关于原始程序有两个问题会影响性能和内存使用.

There are two concerns regarding the original program that will impact performance and memory usage.

调用方法update 以每0.0005 秒设置小部件的文本.这是非常昂贵的.请参考 编程指南 » 事件和属性 » 调度重复事件 &触发事件.

Calling method update to set the widget's text every 0.0005 seconds. This is very expensive. Please refer to Programming Guide » Events and Properties » Scheduling a repetitive event & Trigger events.

class SelectableButton(RecycleDataViewBehavior, Button):
    ...
    def __init__(self, **kwargs):
        super(SelectableButton, self).__init__(**kwargs)
        Clock.schedule_interval(self.update, .0005)

    def update(self, *args):
        self.text = self.rv_data[self.index][self.key]

解决方案 - 问题 1

在类RVupdate方法中添加text.

class RV(BoxLayout):
    ...
    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]

问题 2

col1_datacol2_data 相似,除了 key 的文本和字典(IdName).当数据变大时,应用程序可能会因为内存不足而无法运行,如果运行了,性能就会很差.

Issue 2

col1_data and col2_data are similar except the text and dictonary for key (Id or Name). When the data grows bigger, the app might not run because it ran out of memory, and if it does run, it gives poor performance.

没有提供解决方案,因为它超出了本主题并留给您作为练习.

No solution provided because it is out of this topic and left as an exercise for you.

  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.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("
The 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 TestApp(App):
    title = "test"

    def build(self):
        return MainMenu()


if __name__ == '__main__':
    TestApp().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

输出

这篇关于Python/Kivy:使用“向上"和“向下"键在 RecycleView 中选择行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆