[Kivy] RecycleViewの項目の選択と選択したデータの取得

RecycleViewの項目の選択と取得

 RecycleViewに表示する項目は、マウスでクリックすることで選択可能にすることができます。しかし、RecycleViewでは項目の選択可能にしただけでは、選択した項目とそれ以外の項目とで見た目に違いが生じないので、ユーザーがRecycleViewの選択状況を確認することができません。

 このため、RecycleViewに表示する項目の選択をユーザーに状況がわかるようにおこなうためには、①項目を選択可能にするだけでなく、②ユーザーが選択している項目がどれであるか視覚的に分かるようにする必要があります。

 今回は、RecycleViewに表示するデータを選択可能にすることと、RecycleViewで選択した項目を取得する方法を紹介します。

今回紹介する内容
 ・ Recycleviewの項目を選択可能にする
 ・ Recycleviewの選択されている項目が分かるようにする(背景色の変更)
 ・ RecycleViewで選択したデータを取得する

RecycleViewを複数の項目が選択可能にする場合は以下の記事があります。

RecycleViewの項目を選択可能にする

Recycleviewの項目を選択可能にする

 RecycleViewの項目を選択可能にするには、①RecycleViewのレイアウト(以下のサンプルでは、SelectableRecycleBoxLayout)と、②RecycleViewのビュー(以下のサンプルではRVBox)に対して以下の作業をおこないます。
 1. RecycleViewのレイアウトに、動作を取り扱うクラスを継承する。
 2. RecycleViewのビューのサイズを変更する。
 3. RecycleViewのビューに、選択時の処理を記入する。

1.1. RecycleViewのレイアウトに、動作を取り扱うクラスを継承する。

 RecycleViewの項目を選択可能にするためには、kivy.uix.behaviors.ForcusBehaviorとkivy.uix.recycleview.layout.LayoutSelectionBehaviorをレイアウトに継承します。この作業をKv languageでおこなうと以下のようになります。

<SelectableRecycleBoxLayout@FocusBehavior+LayoutSelectionBehavior+RecycleBoxLayout>

 また、同じ作業をPython側でおこなう場合は以下のように記入します。

from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior

class SelectableRecycleBoxLayout(
 FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout):
    pass

1.2. RecycleViewのビューのサイズを変更する。

 ビュー(viewclass)であるRVBoxは、BoxlayoutにLabelを2つ載せたものであり、Recycleviewに表示される項目は1つずつビューより作成されます。項目は、その範囲内であればどこをクリックしても選択される必要があるため、そのためにはマウスクリックの判定をおこなうBoxlayoutのサイズは、項目と同じである必要があります。しかし、Boxlayoutのサイズはデフォルトでは(100,100)であるため、Boxlayoutのサイズを項目と同じに変更する必要があります。

 Boxlayoutのサイズを項目のサイズに変更するためには、以下の2つの作業をおこないます。

 まず、RecycleViewのレイアウト(以下ではSelectableRecycleBoxLayout)のdefault_sizeにNone, None、size_hintに1, Noneを指定して、Boxlayoutのサイズを変更可能にします。

        RecycleView:
            id: rv
            viewclass: 'RVBox'
            SelectableRecycleBoxLayout:
                id: selectablervl
                default_size: None, None
                size_hint: 1, None
                height: self.minimum_height
                orientation: 'vertical'

 次に、RVBox(Boxlayoutを継承)にsize_hint、width(Boxlayoutに載せる2つのラベルの幅の和), height(Boxlayoutに載せるラベルの高さ)の値を記入して、項目のどこをマウスでクリックしても反応するようにします。

<RVBox>:
    text1: ''
    text2: ''

    id: rv
    size_hint: None, None
    width: self.ids.text1.width + self.ids.text2.width
    height: self.ids.text1.heigh

3. RecycleViewのビューに、選択時の処理を記入する。

 RecycleViewのビューであるRVBoxクラスでは、refresh_view_attrs()とon_touch_down()を定義します。

 refresh_view_attrs()は、項目を作成する時に呼ばれる関数をオーバーライドしたものであり、index番号をRVBoxの変数index入れて、継承元のrefresh_view_attrs()を実行しています。

 on_touch_down()は、マウスをクリックした場合に呼ばれる関数をオーバーライドしたものであり、まず、if super(RVBox, self).on_touch_down(touch):により、マウスクリックをした項目に対して処理が終わっていないか確認して、終わっている場合は以降の処理(マウスクリック位置が項目上であるかの判定)をおこなわないようにしています。マウスクリックがおこなわれると、RecycleViewに表示したすべての項目対して順番にon_touche_down()に記入した処理がおこなわれるので、その作業の負荷を低減するために、対象の項目の処理が終了したら残りの項目に対しては以降の処理をおこなわないようにするためのものです。
 次に、if self.collide_point(*touch.pos):により、マウスクリック位置が項目上であるかの判定をおこない、項目上にある場合は、self.parent.select_with_touch(self.index, touch)によってRecycleViewのレイアウトに、選択した項目のインデックス番号を送り、項目の選択状態を変更します。

class RVBox(RecycleDataViewBehavior, BoxLayout):
    index = None
    selected = BooleanProperty(False)

    def refresh_view_attrs(self, rv, index, data):
        self.index = index
        return super(RVBox, self).refresh_view_attrs(
            rv, index, data)

    def on_touch_down(self, touch):
        if super(RVBox, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos):
            return self.parent.select_with_touch(self.index, touch)

Recycleviewの選択されている項目が分かるようにする(背景色の変更)

 Recycleviewで選択した項目を分かるようにするには、選択項目の背景色を変えるて他の項目よりも目立つようにするのが良いと思います。そのためには以下の作業をおこないます。
 1. Kv languageで<RVBox>の背景色を、selectedの値で変更するように指定する。
 2. RVBoxのクラスに変数selectedとapply_selection定義する。 

2.1. Kv languageで<RVBox>の背景色を、selectedの値で変更するように指定する。

 以下では、選択した項目の背景だけが青色調になるようにするため、selectedの値がTrueの場合はColorを(0.0, 0.2, 0.9, 0.5)、Falseの場合は(0, 0, 0, 0)にするように指定しています。

<RVBox>:
  # 省略
    canvas.before:
        Color:
            rgba: (0.0, 0.2, 0.9, 0.5) if self.selected else (0, 0, 0, 0)
        Rectangle:
            pos: self.pos
            size: self.size

2.2. RVBoxのクラスに変数selectedと関数apply_selection定義する。

 関数apply_selectionは項目の選択が切り替わった場合に呼び出される関数であり、項目を変更すると①選択した項目と②その前に選択されていた項目に対してこの関数が実行されます。
 そこで、関数apply_selectionでは、その項目の選択状態の情報であるis_selectedを、変数selectedに渡す処理をおこないます。これにより、①選択した項目と②その前に選択されていた項目の変数selectedの値が更新され、1.の<RVBox>内で指定した背景色のルールに則り項目の背景色が変更されます。

class RVBox(RecycleDataViewBehavior, BoxLayout):
    index = None
    selected = BooleanProperty(False)
    selectable = BooleanProperty(True)

  # 省略

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

RecycleViewで選択したデータを取得する

 RecycleViewでは選択した項目の番号は、selected_nodesにリスト形式で保存されます。そのため、単一の項目のみを選択可能な場合は、selected_nodes[0]により選択中のデータの番号を取得できます。その番号を使用して、self.ids.rv.dataよりそのデータを得ることができます。

    def on_button_press(self):
        select_index = self.ids.selectablervl.selected_nodes[0]
        print(f"{select_index}: {self.ids.rv.data[select_index]}")

サンプルプログラム

 以下のサンプルプログラムは、実行すると左図のようなRecycleViewとその下にボタンが配置された画面が表示されます。RecycleView上のデータはマウスでクリックすることで選択可能であり、選択したデータの背景は、右図のように青色になります。
 また、データを選択した状態でボタンを押すと、ターミナルに選択中のデータを出力します。

# -*- coding: utf-8 -*-

import kivy

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.config import Config
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.properties import BooleanProperty

kivy.require('2.2.0')

Config.set('graphics', 'width', '450')
Config.set('graphics', 'height', '400')

Builder.load_string('''

<MainD>
    BoxLayout:
        orientation: 'vertical'
        size: root.size
        padding: 10

        RecycleView:
            id: rv
            viewclass: 'RVBox'
            SelectableRecycleBoxLayout:
                id: selectablervl
                default_size: None, None
                size_hint: 1, None
                height: self.minimum_height
                orientation: 'vertical'

        Button:
            text: 'Get Data'
            size_y: None
            size_hint_y: 0.2
            on_press: root.on_button_press()

<SelectableRecycleBoxLayout@FocusBehavior+LayoutSelectionBehavior+RecycleBoxLayout>

<RVBox>:
    text1: ''
    text2: ''

    id: rv
    size_hint: None, None
    width: self.ids.text1.width + self.ids.text2.width
    height: self.ids.text1.height
    canvas.before:
        Color:
            rgba: (0.0, 0.2, 0.9, 0.5) if self.selected else (0, 0, 0, 0)
        Rectangle:
            pos: self.pos
            size: self.size


    Label:
        id: text1
        size_hint: None, None
        size: 200, 60
        text: root.text1

    Label:
        id: text2
        size_hint: None, None
        size: 200, 60
        text: root.text2



''')


class RVBox(RecycleDataViewBehavior, BoxLayout):
    index = None
    selected = BooleanProperty(False)

    def refresh_view_attrs(self, rv, index, data):
        self.index = index
        return super(RVBox, self).refresh_view_attrs(
            rv, index, data)

    def on_touch_down(self, touch):
        if super(RVBox, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos):
            return self.parent.select_with_touch(self.index, touch)

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


class MainD(Widget):

    def __init__(self, **kwargs):
        super(MainD, self).__init__(**kwargs)
        self.ids.rv.data = [
            {'text1': 'text1 = 1', 'text2': 'text2 = 4'},
            {'text1': 'text1 = 2', 'text2': 'text2 = 3'},
            {'text1': 'text1 = 3', 'text2': 'text2 = 2'},
            {'text1': 'text1 = 4', 'text2': 'text2 = 1'},
            {'text1': 'text1 = 1', 'text2': 'text2 = 4'},
            {'text1': 'text1 = 2', 'text2': 'text2 = 3'},
            {'text1': 'text1 = 3', 'text2': 'text2 = 2'},
            {'text1': 'text1 = 4', 'text2': 'text2 = 1'},
            ]

    def on_button_press(self):
        select_index = self.ids.selectablervl.selected_nodes[0]
        print(f"{select_index}: {self.ids.rv.data[select_index]}")


class MainDApp(App):
    def __init__(self, **kwargs):
        super(MainDApp, self).__init__(**kwargs)
        self.title = 'RecycleView Test4'

    def build(self):
        return MainD()


if __name__ == '__main__':
    app = MainDApp()
    app.run()
タイトルとURLをコピーしました