Kivyで検索ボックス


 
#目次
ソースコードは[ここ](https://github.com/Naotonosato/MatchView/find/master?q=)にあります。

1. はじめに
2. Recycle View
3. 一致する場所をマークアップする 
4. View Classの作成
5. 完成
6. さいごに

#はじめに
本記事ではタイトルの通り、PythonのGUIライブラリーのKivyを使用して入力された文字に一致する項目を逐次表示する検索ボックスを作成していきます。
イメージとしてはGitHubのファイルの検索ボックスのような感じです。
#Recycle View
RecycleViewについて説明します。
RecycleViewはMVCデザインパターンに基づいて設計されたクラスで、ListViewの後継だそうです。
基本的には

 1. MVCのM(model)に値するdataプロパティを設定する
 + ビュークラスを指定する
 + 子にRecycleBoxLayputなどのRecycleView用のレイアウトウィジェットを追加する
 
ことで使用できます。
例を見てみましょう。
test.py

```python
from kivy.uix.recycleview import RecycleView
from kivy.app import App

class MyView(RecycleView):
    def __init__(self,**kwargs):
        super(MyView,self).__init__(**kwargs)
        self.data = [{'text':'1'},{'text':'2'}] #1
        self.viewclass = Button #2
class MyApp(App):
    def build(self):
        return MyView()
```

My.kv

```
MyView:
    RecycleBoxLayout: #3
        orientation: 'vertical'
        default_size_hint: 1,None
        default_size: None,dp(45)
        size_hint_y: None
        spacing: 5
        height: self.minimum_height
        padding: 10
```

解説：
まずはdataプロパティについて。dataプロパティは辞書のリストです。それぞれの辞書にはviewclass（ここではButton）のプロパティ名とその値を指定できます。ですので例えばBubbleをviewclassにした場合、{'arrow_pos':'left_top'}のようにしてarrow_posプロパティを指定できます。
次はviewclassについて。viewclassは読んで字のごとく、MVCのV(view)にあたる部分の表示をどのクラスでするか、ということを指定できます。
最後に子に追加するレイアウトウィジェットについて。今回はRecycleBoxLayoutを使用しました。これを追加することによってデータ表示の形式を決められます。

#一致する場所をマークアップする
KivyではLabelクラス等のmarkupプロパティをTrueにすることで、テキストをマークアップできます。
今回は検索ワードと一致した場所を濃く表示するために使います。 
それではこれを利用して一致部分を濃く表示する機能をもったRecycleViewのサブクラスを実装していきましょう。
MacthView.py

```python
from kivy.uix.recycleview import RecycleView

class MatchView(RecycleView):

    def __init__(self,**kwargs):
        super(MatchView,self).__init__(**kwargs)

        self.titles     = ['aiueo','tyuukansikenyada','marasonnkirai']
        self.match_data = {}

    def match(self,word):

        self.match_data = {}
        
        for title in self.titles:
            i = 0
            if title.find(word) >= 0:
                if not self.match_data.get(word):
                    self.match_data[word] = {}
                if not self.match_data[word].get(title):
                    self.match_data[word][title] = {title:[]}
                while True:
                    index = title.find(word,i)
                    if index >= 0:
                        i += index+len(word)
                        self.match_data[word][title][title].append(index)
                    else:
                        break

        self.data=[]
        for w in self.match_data:
            for t in self.match_data[w]:
                indexes = self.match_data[w][t][t][::-1]
                for i in indexes:
                    t = t[:i]+'[color=#AAAAAA]'+w+'[/color]'+t[i+len(w):]
      
                self.data.append({'text':t,'color':(0,0,0,0.2),'markup':True})

```
解説：
検索ワードと一致する場所を探すためのメソッド、matchを定義しています。
前半のfor文では{word:{title:{title:[index1,index2...]}}}といった形の辞書を作成し、self.match_dataに代入しています。
後半ではそれぞれのタイトルごとに一致した場所に[color]タグを挿入する事でほかの場所と色を変えています。
ただし前の方からタグを挿入してしまうと、たとえば
title = '123531234'
indexes = [0,5]
word = '1'
のような場合に
'[colo[color=#AAAAAA]1[/color]r=AAAAAA]1[/color]23531234'
というむちゃくちゃな文章になってしまいますので[::-1]とスライスをうまく使ってリストを逆順にすることでその問題を解決しています。（indexes = indexes.sort(reverse=True)でも同じことができます）

※10/26　追記 リスト内法表記を使うと良い、とのご指摘を受けましたのでリスト内法表記を使ったプログラムを掲載しておきます。

```python
def match(self.word):
    self.data = [{'text':title.replace(word,'[color=#AAAAAA]'+word+'[/color]'),
                    'color':(0,0,0,0.2),'markup':True}
                     for title in self.titles 
                     if title.find(word.lower()) >= 0 or title.find(word.upper()) >= 0]

```

この書き方の方が高速です。多分。


#View Classの作成
MatchView.pyを作成しましたが、viewclassの指定とレイアウトウィジェットの追加がまだでした。
以下のMatchView.kvにてそれらを追加していきます。

MatchView.kv

```
MatchView:
	id: matchview
	size_hint_y: 0.5
	viewclass: 'MyLabel'
	RecycleBoxLayout:
		orientation: 'vertical'
		default_size_hint: 1,None
		default_size: None,dp(45)
		size_hint_y: None
		scaping: 5
		height: self.minimum_height
		padding: 10
```

これでviewclassとレイアウトウィジェットが追加できました。
しかし！このままでは各ラベルに境界線もなく文字がつらつら表示されているだけなので見た目が微妙です。
なので独自に表示用のラベルを定義してしまいましょう。

```
<MyLabel@Label>:
    canvas:
        Color:
            rgba: 0,0,0,0.6
        Line:
            points: (self.x,self,y),(self.x+self.width,self.y)
```
これで見た目も良くなります。
因みにMyLabelで使った方法（キャンバスの下部にLineで線を引く）は結構よく使うので覚えておいて損はないかと思います。

それでは今まで定義してきたクラスやビューをひとまとめにして実行できるようにしてみましょう。
MatchView.pyに以下のコードを追加します。

```python
from kivy.app import App

MacthViewApp(App):
    pass

MatchViewApp().run()
```
そしてMatchView.kvを以下のように変更します。

```
Root

<MyLabel@Label>:
    canvas:
        Color:
            rgba: 0,0,0,0.6
        Line:
            points: (self.x,self.y),(self.x+self.width,self.y)
    
<Root@BoxLayout>:
	size: root.size
	orientation: 'vertical'
	padding: self.width * 0.2,self.height * 0.15
	matchview: matchview
	canvas: 
		Color:
			rgba: 0.99,0.99,0.99,1
		Rectangle:
			size: self.size
			pos: self.pos

	TextInput:
		size_hint_y: 0.1
		font_size: self.height/2
		on_text: 
			if self.text: root.matchview.match(self.text)
			else: root.matchview.data = []
	MatchView:
		id: matchview
		size_hint_y: 0.5
		viewclass: 'MyLabel'
		RecycleBoxLayout:
			orientation: 'vertical'
			default_size_hint: 1,None
			default_size: None,dp(45)
			size_hint_y: None
			scaping: 5
			height: self.minimum_height
			padding: 10

	Widget:
		size_hint_y: 0.3
	Button:
		size_hint_y: 0.1
		text: 'Go' #dumb
```


#完成
完成です！
以下のようになるかと思います。![Spie 2018_09_20 20_46_18.png](https://qiita-image-store.s3.amazonaws.com/0/285911/15ca1a17-7e27-65ce-a77a-6f54c921ab0b.png)

#さいごに
いかがでしたか？
今回は逐次表示検索ボックス（という名前かは分かりませんが）の作成という形でKivyについて解説させていただきました。

冒頭の通り、この記事は僕の初めての投稿です。おかしいな、と思うところがあったらぜひご指摘お願いします。