あらまし
過去2回の記事(「Kv Languageの基本」,「電卓を作成する」)を読むと
Kivyで簡単な画面が1枚のアプリの作成ができるようになったかと思います。
今回はマルチ画面(2画面)のアプリを作ります。
作成するもの
今回、作成するものはWebAPIを使用して検索条件とその結果を送受信して、
結果を一覧表示して、選択した項目の詳細な内容を表示するようにします。
以下、実際の画面です。
起動画面(一覧表示)
詳細画面
一覧表示から、右に画面をドラッグまたはアクションバーの「詳細」ボタンをクリックすると、詳細画面が横からスライドして表示されます。
検索していないので項目は何も表示されません。
実際の検索結果
検索フォームに条件を入力して、「検索」ボタンを押した結果です。
検索フォームの下に検索結果の一覧が書籍名の一覧表示で表示されます。
項目をクリックすると、自動で詳細画面に遷移します。
詳細画面(検索結果選択時)
詳細画面に書籍名とそれに付随する情報が表示されます。
画面を左ドラッグ、またはアクションバー右上の「一覧」ボタンを選択すると、
一覧画面に戻ります
一覧画面(検索語、詳細画面から遷移時)
一覧画面に戻ると、一覧から選択した項目が選択状態(赤枠)になっているのがわかります。
あらたに習得する内容
あらたに習得する内容は大きく、以下になります
- Carouselによるマルチ画面の作り方
- Spinnerによるリスト選択の作り方
- Llistviewによる一覧表示の作り方※注)
- ListButtonによる一覧表示からのボタンの作りかた
- Adaptersによる、検索結果からListへのコンバートの仕方
※注) ListViewは次期バージョンの1.10.2でされ、代わりに RecycleViewを使用することが推奨されています。
ただし今回は1.9.2がリリースされていないことと、RecycleViewの仕様がはっきりししていないために、ListViewを使用します。
同様にAdaptersも開発中などの理由で、現在は推奨されていませんが、代変えのAPIが提示されていないのでこちらを使用します。
※2017/10/25 追記
Kivy1.10がリリースされましたが、listviewは1.10でも動作は可能です。一方RecycleViewに関してはリリースがされましたがいくつのissueがあげられたり、実行のサンプルが少ないので当面はRecycleViewを使用するのではなくlistviewを使用することを個人的にはお勧めします。
参考リンク
参考にしたものをあげます。
-
Creating Apps in Kivy - O'Reilly Media
オライリーが2014年に出版したKivyの入門書です。Kivyの基本的な使い方から、OpenWeatherMap(天気情報をAPIで提供しているサービス)を使用して、リクエストを飛ばし、各都市の天気予報を取得し表示するのを使用して結果を表示する。さらにAndroid、IOSなどのモバイルアプリに書き出すところまでやっています。
この本が邦訳されていれば国内でKivyよくわからないという声は上がらなかったのではと思います。
ビデオによるListViewの解説です。
検索する内容について
今回は国会図書館のサーチの外部提供APIを使用します。CQLという検索クエリを組み立てて送信すると蔵書結果が返ってきます。ただし、一からCQLを組み立てるのは結構手間なのと、今回は結果の送受信したデータの取扱いをメインとしたいのでこちらを使用します。
ライブラリの中身はPythonでCQLの検索クエリを組み立てて、Pythonのライブラリであるrequestsを使用して検索結果を送受信して結果表示しています。
参考
- [Pythonで国立国会図書館サーチAPIを使用] (http://qiita.com/noco/items/c2d1f4308ee7a50c462e)
- pyndlsearch(ソース)
プログラムについて
Kivyを使用したコードの書き方ですが、色々な書き方があります。あくまでもこれは一例です。また、今回の記事を書く際に使用したソースコードをGithubに挙げております。ただし、fontや画像などの素材は配置しておりませんので必要な際はご自分で用意して配置してください。
検証環境
検証環境は以下の通りです。
OS: Windows10 64bit
Kivy:1.9.1
Python3.4※
※Kv Languageファイルですが、Python3系の場合は”Shift-jis”で保存してください。
以下、実際にコードと結果を載せていきます。
コード
Python側のコードは以下の通りです。
#-*- coding: utf-8 -*-
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty, ListProperty
from kivy.uix.listview import ListItemButton
from kivy.core.text import LabelBase, DEFAULT_FONT
from kivy.resources import resource_add_path
# 国立国会図書館サーチ外部提供インタフェース(API)
from pyndlsearch.client import SRUClient
from pyndlsearch.cql import CQL
# デフォルトに使用するフォントを変更する
resource_add_path('./fonts')
#resource_add_path('/storage/emulated/0/kivy/calc/fonts')
LabelBase.register(DEFAULT_FONT, 'mplus-2c-regular.ttf') #日本語が使用できるように日本語フォントを指定する
class BookButton(ListItemButton):
''' search_results(ListView)の項目をボタンにする '''
book = ListProperty()
class SearchBookForm(BoxLayout):
search_input = ObjectProperty()
search_results = ObjectProperty() # kvファイル側のsearch_results(ListView)を監視
def __init__(self, **kwargs):
super(SearchBookForm, self).__init__(**kwargs)
def books_args_converter(index, data_item):
''' 検索結果を book名をキーとした辞書型に変換する。
検索結果のレコード分呼ばれて実行される。
'''
title, creater , language, publisher = data_item
return {'book': (title, creater , language, publisher )}
def search_book(self):
''' 検索条件をもとに検索し、結果をListViewに格納する '''
print('search_book')
cql = CQL()
# ★検索条件を入力していく
cql.title = self.search_input.text
year = self.ids['year'].text
month = self.ids['month'].text
day = self.ids['day'].text
cql.fromdate = year + '-' + month + '-' + day # 出版年月日
#cql.fromdate = '2000-10-10'
#print(cql.payload())
#cql.title = 'Python'
#cql.fromdate = '2000-10-10'
# NDL Searchクライアントの設定
client = SRUClient(cql)
client.set_maximum_records(int(self.ids['number'].text)) #最大取得件数
# ★検索条件入力終了
#client.set_maximum_records(10) #最大取得件数
#print(client)
# get_response()ではxml形式で取得可能
#res = client.get_response()
#print(res.text)
# SRU実行(入力条件を元に検索を実行する)
srres = client.get_srresponse()
# 検索結果をbooksリストに格納
books = [(d.recordData.title, d.recordData.creator, d.recordData.language, d.recordData.publisher) for d in srres.records]
print(books)
print("----------------")
# 検索結果に格納する
self.search_results.adapter.data.clear() # 検索結果をdata(詳細表示用)を消去
self.search_results.adapter.data.extend(books) # 検索結果をdataに追加
self.search_results._trigger_reset_populate() # search_results(list_view)をリフレッシュ
class BookInfo(BoxLayout):
''' 詳細画面の情報 '''
book = ListProperty(['', '','',''])
class BookSearchRoot(BoxLayout):
def __init__(self, **kwargs):
super(BookSearchRoot, self).__init__(**kwargs)
def show_book_info(self, book):
''' 選択した情報を整形して詳細画面へ移動して表示する '''
print('BookSearchRoot')
print(book) #Book = BookButton()の値、取れているか確認用
# LabelのTextにNoneが入るとエラーになるために変換を行う
book_convs = [x if x != None else '' for x in book] # Noneが返ってきた場合は""に変更する
# 詳細画面に書籍情報を格納
self.bookinfo.book = book_convs
# 詳細画面に移動
self.carousel.load_slide(self.bookinfo)
class BookSearchApp(App):
def __init__(self, **kwargs):
super(BookSearchApp, self).__init__(**kwargs)
self.title = '国会図書館検索'
pass
if __name__ == '__main__':
BookSearchApp().run()
Kvファイル
Kv Languageは以下になります。
#: import main main
#: import ListAdapter kivy.adapters.listadapter.ListAdapter
# 起動時に表示されるwidget
BookSearchRoot
<BookSearchRoot>
# 一覧画面
carousel: carousel
booklists: booklists
bookinfo: bookinfo
BoxLayout:
orientation: "vertical"
ActionBar:
ActionView:
ActionPrevious:
title: "国会図書館検索"
with_previous: False
app_icon: "./icon/32player.png"
ActionButton:
text: "一覧"
# 一覧画面に移動する
on_press: app.root.carousel.load_slide(app.root.booklists)
ActionButton:
text: "詳細"
# 詳細画面に移動する
on_press: app.root.carousel.load_slide(app.root.bookinfo)
Carousel:
id: carousel
SearchBookForm: # 一覧画面
id: booklists
BookInfo: # 詳細画面
id: bookinfo
<SearchBookForm>
# 一覧画面のレイアウト
orientation: "vertical"
search_input: search_box # ①クラス変数を追加 こうすることでPython側でself.search_inputが取れる
search_results: search_results_list
# 検索フォーム
BoxLayout:
height: "40dp"
size_hint_y: None
TextInput:
id: search_box # ② 「①」で値を渡せる
size_hint_x: 70
Button:
text: "検索"
size_hint_x: 30
on_press: root.search_book()
BoxLayout:
size_hint:1,.1
Label:
size_hint: .2,1
text: "出版年月"
Spinner: # 年のリスト表示
id: year
size_hint: .1,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'2000'
values: [str(y) for y in range(2000, 2018) ]
Label:
size_hint: .05,1
text: "年"
Spinner:
id: month # 月のリスト表示
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'01'
values: ['{0:02d}'.format(x) for x in range(1,13)]
Label:
size_hint: .05,1
text: "月"
Spinner: # 日のリスト表示
id: day
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'01'
values: ['{0:02d}'.format(x) for x in range(1,30)] # 月ごとに日が変わる処理が必要だが一旦保留
Label:
size_hint: .05,1
text: "日"
Label:
size_hint: .05,1
text: "件数"
Spinner: # 月ごとのサイズ
id: number
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'10'
values: ['1','5','10', '15', '20']
ListView:
id: search_results_list
adapter:
# 検索結果を一覧表示かつ項目をボタンにする
# data = 検索の一覧をリストで保持する
# CLS = 一覧の表示形式(今回はボタンにして表示する)
# args_converter = 表示結果を書籍名をキーにしたリストに変換する.
ListAdapter(data=[], cls=main.BookButton, args_converter=main.SearchBookForm.books_args_converter)
<BookButton>
# 検索結果をボタンにするレイアウト
text_size: self.size
halign: 'left'
text: self.book[0] #書籍名をボタンのタイトルにする
height: "40dp"
size_hint_y: None
on_press: app.root.show_book_info(self.book)
<BookInfo>
# 検索結果
book: ("","","","")
orientation: "vertical"
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: "40dp"
GridLayout:
cols: 2
rows: 4
Label:
text: "タイトル"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text_size:self.size
halign: 'left'
valign: 'middle'
text:root.book[0]
size_hint_x: 80
Label:
text: "作者"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[1]
size_hint_x: 80
text_size:self.size
halign: 'left'
valign: 'middle'
Label:
text: "出版社"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[3]
size_hint_x: 80
#text: "出版社:{} ".format(root.book[3])
text_size:self.size
halign: 'left'
valign: 'middle'
Label:
text: "言語"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[2]
size_hint_x: 80
text_size: self.size
halign: 'left'
valign: 'middle'
解説
解説は、Kv Languageから始めます。
「BookSearchRoot」widgetについて
起動時は「BookSearchRoot」widgetが表示されます。「BookSearchRoot」大きく分けて2つのwidgetで成り立っています。
該当のKvは以下になります。
該当のコードは以下の通りです。
```py3
<BookSearchRoot>
# 一覧画面
carousel: carousel
booklists: booklists
bookinfo: bookinfo
BoxLayout:
orientation: "vertical"
ActionBar:
ActionView:
ActionPrevious:
title: "国会図書館検索"
with_previous: False
app_icon: "./icon/32player.png"
ActionButton:
text: "一覧"
# 一覧画面に移動する
on_press: app.root.carousel.load_slide(app.root.booklists)
ActionButton:
text: "詳細"
# 詳細画面に移動する
on_press: app.root.carousel.load_slide(app.root.bookinfo)
Carousel:
id: carousel
SearchBookForm: # 一覧画面
id: booklists
BookInfo: # 詳細画面
id: bookinfo
Carouselについて
Carouselはスワイプすることで、画面(スライド)を切り替えるwidgetです。
今回は、2画面(SearchBookForm,BookInfo)を操作します。使い方はwidget名とidを使用して操作します。また以下のようにすることで見せたい画面を自動的にスワイプします。
carousel.load_slide(表示したいスライドのid)
他にも切り替わりの速度の変更などのプロパティがあるので詳しくはAPIリファレンスを参考にしてください。
参考
前回は画面の切り替わりに「clear_widgets()」を使用して画面(widget)を一旦削除し、「add_widgets()」を使用して画面(widget)を作成しました。
このやり方だと問題としては前の画面に戻るのが難しいです。
widgetを削除してしまうので値の保持も難しいです。Carouselを使用することで各画面で値を保持したまま、複数の画面に遷移して行き来できます。
アクションバー
画面上部のアクションバーは以下の画面になっています。
該当のコードは以下の通りです。
ActionBar:
ActionView:
use_separator: True
ActionPrevious:
title: "国会図書館検索"
with_previous: False
app_icon: "./icon/32player.png"
ActionButton:
text: "一覧"
# 一覧画面に移動する
on_press: app.root.carousel.load_slide(app.root.booklists)
ActionButton:
text: "詳細"
# 詳細画面に移動する
on_press: app.root.carousel.load_slide(app.root.bookinfo)
このうち画面左側のタイトル部分ですが、今回はApp_iconに独自のアイコンを指定して表示させています。アイコン素材は元のKivyのアイコンのサイズが32px×32pxなのでサイズはそれに習いました。
またボタンを押すことで、各画面に移動できます。
このプログラムを動かすとわかりますが、リスト表示や画面を一覧から詳細に切り替えてもActionBarの部分は画面から消えずに表示され続けます。これはBoxLayoutを使用してActionBarとCarousel部分を分けているためです。画面の切り替わりや検索結果のリスト表示はCarousel上で表示されています。
SearchBookFormについて
SearchBookFormは大きく分けて、2つに分かれます。
1つは、検索条件を入力する検索フォーム部分、もう一つは検索結果を一覧表示する部分です・
検索フォーム部分について
画面は以下の通りです。
またフォームを構成しているKv部分は以下の通りです。
# 検索フォーム
BoxLayout:
height: "40dp"
size_hint_y: None
TextInput:
id: search_box # ② 「①」で値を渡せる
size_hint_x: 70
Button:
text: "検索"
size_hint_x: 30
on_press: root.search_book()
BoxLayout:
size_hint:1,.1
Label:
size_hint: .2,1
text: "出版年月"
Spinner: # 年のリスト表示
id: year
size_hint: .1,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'2000'
values: [str(y) for y in range(2000, 2018) ]
Label:
size_hint: .05,1
text: "年"
Spinner:
id: month # 月のリスト表示
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'01'
values: ['{0:02d}'.format(x) for x in range(1,13)]
Label:
size_hint: .05,1
text: "月"
Spinner: # 日のリスト表示
id: day
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'01'
values: ['{0:02d}'.format(x) for x in range(1,30)] # 月ごとに日が変わる処理が必要だが一旦保留
Label:
size_hint: .05,1
text: "日"
Label:
size_hint: .05,1
text: "件数"
Spinner: # 月ごとのサイズ
id: number
size_hint: .05,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'10'
values: ['1','5','10', '15', '20']
このうち、TexInput()については、文字入力部分で前回までに説明しました。
ちなみにWindowsOSで文字入力でIMEが開かないのはKivyのバグです。 日本語で検索する場合はメモ帳などで入力した値をコピペして使用してください。
次に、年月日の一覧を選択するのに今回は「Spinner」を使用しています。Spinnerはクリックするとリストが表示され、そこから項目を選択することができます。
年の該当のKvは以下の通りです。
Spinner: # 年のリスト表示
id: year
size_hint: .1,1
halign: 'center'
valign: 'middle'
text_size: self.size
text:'2000'
values: [str(y) for y in range(2000, 2018) ]
Valuesに入れた値がリストになります。
詳しくはAPIリファレンスを参考にしてください。
Spinner
一覧画面表示部分について
次に、検索結果の一覧表示をしている部分の説明
またフォームを構成しているKv部分は以下の通りです。
ListView:
id: search_results_list
adapter:
# 検索結果を一覧表示かつ項目をボタンにする
# data = 検索の一覧をリストで保持する
# CLS = 一覧の表示形式(今回はボタンにして表示する)
# args_converter = 表示結果を書籍名をキーにしたリストに変換する.
ListAdapter(data=[], cls=main.BookButton, args_converter=main.SearchBookForm.books_args_converter)
ここでまずListViewについて説明します。
ListView
ListViewはデータをリスト形式で表示するためのwidgetです。
簡単な使い方は、「item_strings」と呼ばれるプロパティにリスト構造を入力します。
例えば以下のコードの場合、item_stringsには0から100までの連続した数が入り、実行すると、0から100までの項目がLabel形式で表示されます。
class MainView(ListView):
def __init__(self, **kwargs):
super(MainView, self).__init__(
item_strings=[str(index) for index in range(100)])
ただし、この形式で表示されるのはあくまでもLabelなので選択して動作をすることができません。
そこで今回は、Adaptersというメソッドを使用して一覧をボタン表示に変えます。
ListAdapter(data=[], cls=main.BookButton, args_converter=main.SearchBookForm.books_args_converter)
引数の説明は以下になります。
- data:検索結果の情報をリストで保持する
- cls: 一覧をindexにして表示します(今回はボタンに設定して表示)
- args_converter = 検索結果のレコードを1件ずつ書籍名をキーにしたリストに変換する
各項目のPython側のファイルと照らし合わせて説明します。
data
dataは検索結果の値を保持します
今回ですと、検索すると以下の用なタプルが返ってきます。
該当のコードは以下の通りです。
def search_book(self):
''' 検索条件をもとに検索し、結果をListViewに格納する '''
~ 省略 ~
# SRU実行(入力条件を元に検索を実行する)
srres = client.get_srresponse()
# 検索結果をbooksリストに格納
books = [(d.recordData.title, d.recordData.creator, d.recordData.language, d.recordData.publisher) for d in srres.records]
print(books)
print("----------------")
# 検索結果に格納する
self.search_results.adapter.data.clear() # 検索結果をdata(詳細表示用)を消去
self.search_results.adapter.data.extend(books) # 検索結果をdataに追加
self.search_results._trigger_reset_populate() # search_results(list_view)をリフレッシュ
booksリストに格納される値は以下の通りです。
[('10日でおぼえるPython入門教室', '穂苅実紀夫, 寺田学, 中西直樹, 堀田直孝, 永井孝 著', 'jpn', '翔泳社'),
~省略~
('Bluetooth接続のおもちゃを制御する ロボット専用ツールで操作 Pythonで自動化', None, 'jpn', '')]
以下の該当の箇所でdataの前回の結果を削除して、新たに配置しています。
その後「_trigger_reset_populate」で表示を更新しています。
self.search_results.adapter.data.clear() # 検索結果をdata(詳細表示用)を消去
self.search_results.adapter.data.extend(books) # 検索結果をdataに追加
self.search_results._trigger_reset_populate() # search_results(list_view)をリフレッシュ
args_converter
def books_args_converter(index, data_item):
''' 検索結果を book名をキーとした辞書型に変換する。
検索結果のレコード分呼ばれて実行される。
''
title, creater , language, publisher = data_item
return {'book': (title, creater , language, publisher )}
args_converterは検索結果の書籍情報をキーにしてargs_converterに格納しています。
cls
class BookButton(ListItemButton):
''' search_results(ListView)の項目をボタンにする '''
book = ListProperty()
clsは一覧の表示形式を設定します。今回は書籍の情報をList化してbookに格納してButtonに表示します。なお、clsはおそらくThe clear screenの略で画面消去して再表示の意味で用いられていると思われます。
表示の形式に関してはKv側の以下のプロパティを使用します。
<BookButton>
# 検索結果をボタンにするレイアウト
text_size: self.size
halign: 'left'
text: self.book[0] #書籍名をボタンのタイトルにする
height: "40dp"
size_hint_y: None
on_press: app.root.show_book_info(self.book)
Adpterの概念、使用は難しくて私にも完全には把握できていませんが、「data」,「cls」,「args_converter」に値を設定することで一覧の表示形式の設定を変更できることがわかれば一旦大丈夫です。
参考
「BookInfo」widgetについて
詳細画面の説明です。
Kvファイルは以下の通りです。
<BookInfo>
# 検索結果
book: ("","","","")
orientation: "vertical"
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: "40dp"
GridLayout:
cols: 2
rows: 4
Label:
text: "タイトル"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text_size:self.size
halign: 'left'
valign: 'middle'
text:root.book[0]
size_hint_x: 80
Label:
text: "作者"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[1]
size_hint_x: 80
text_size:self.size
halign: 'left'
valign: 'middle'
Label:
text: "出版社"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[3]
size_hint_x: 80
#text: "出版社:{} ".format(root.book[3])
text_size:self.size
halign: 'left'
valign: 'middle'
Label:
text: "言語"
halign: 'left'
valign: 'middle'
size_hint_x: 20
text_size:self.size
Label:
text: root.book[2]
size_hint_x: 80
text_size: self.size
halign: 'left'
valign: 'middle'
「BookButton」widgetをクリックしてmain.show_book_info()が実行されます。
show_book_info()ではbookの情報を整形して、「BookInfo」widget(詳細画面)が表示されます。
ここでは結果を表示するだけで特に難しいことをやっていません。
唯一新規でやっているのは文字を左端にそろえて折り返して表示することでしょうか。
Label:
text_size:self.size
halign: 'left'
valign: 'middle'
text:root.book[0]
size_hint_x: 80
Labelのテキストは、デフォルトのままだと、長い行だとLabelからはみ出して1行に表示されるため、「text_size」でサイズをラベルの大きさと同じに指定しています。そのうえで 「halign」で横方向の表示を左寄せ、「 valign」で縦方向の表示を真ん中に表示します。
参考
まとめ
今回で、複数の画面の表示、複数の入力項目を元にした検索、結果の一覧表示、各項目の詳細表示の方法がわかったかと思います。今回までボタンを使用したデスクトップアプリに関しては作り方がなんとなくわかったかと思います。
次回はこのプログラムを元にAndroid端末で表示してみます。
ちなみに、ここからAndroidに表示するまでにプログラムを大きく変更する必要があり、結構大変でした。
#参考 ファイルのパッケージ化について
Kivyを使用してファイルのパッケージ化をしたい場合はPyInstallerをしようしてパッケージ化ができます。windowsでexe化をしたい場合は別途PyWin32をインストールする必要があります。
参考
- KivyとPyIntstllerのパッケージ化について
- Windows10で、Kivyを使う
- Programming Guide(翻訳済み) » Create a package for Windows(翻訳済み)
この内容の続き
新しく記事を投稿しました。