PythonでTUIアプリを作ろう ( その5 ListBoxで一覧表示 )
UrwidのListBoxオブジェクトを使ってみよう。
「画面に複数行を一覧表形式で表示する」のは、アプリケーションでは標準的な仕組みですが、Urwidでは、ListBoxがこの機能を担います。今回は、その概要について説明しましょう。
単純なListBoxを作ってみる
まずは、前回も使用したクラスのbodyに、ListBoxを作成していきます。ListBoxクラスの生成は、下記のようになります。
class urwid.ListBox(ListWalker)
ListWalkerは、SimpleListWalkerのように、事前に用意されているクラスを使用するのが簡単ですが、ここでは応用の効くListWalkerを自前で書いてみましょう。と言っても難しい話ではなく、
- get_focus()
- set_focus()
- get_next()
- get_prev()
の4メソッドを実装したクラスを用意して、適切なデータを引き渡せば良いだけです。
スクリプトを下記に示します。
データは、仮のものを100件ほど渡すこととします(get_items()メソッド)。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# s05_01.py(ListBoxを使ってみる)
#
import urwid
def get_items():
items = []
for i in range(100):
items.append("テストデータ"+str(i))
return items
my_palette = [
('body', 'default', 'default'),
('foot', 'white', 'dark blue'),
('button', 'yellow', 'default'),
('button_focus', 'black', 'yellow'),
('listwalker', 'black', 'light cyan')
]
def create_mybutton(label, callback, user_data=None):
button = urwid.Button(label, on_press=callback)
return urwid.AttrMap(button, 'button', 'button_focus')
class ListWalker(urwid.ListWalker):
def __init__(self, items):
self.focus = 0
self.create_page(items)
def create_page(self, items):
self.lines = []
for item in items:
text = urwid.Text(item)
self.lines.append(urwid.AttrMap(text, None, 'listwalker'))
def get_focus(self):
return self.get_at_pos(self.focus)
def set_focus(self, focus):
self.focus = focus
self._modified()
def get_next(self, start):
return self.get_at_pos(start + 1)
def get_prev(self, start):
return self.get_at_pos(start - 1)
def get_at_pos(self, pos):
if pos < 0:
return None, None
if len(self.lines) > pos:
return self.lines[pos], pos
return None, None
class Application:
main_loop = None
def __init__(self):
pass
def exit(self, button=None):
raise urwid.ExitMainLoop()
def unhandled_keypress(self, k):
'''
if self.main_loop.widget.focus_position == 'header' and k == 'down':
self.main_loop.widget.focus_position = 'body'
elif self.main_loop.widget.focus_position == 'body' and k == 'up':
self.main_loop.widget.focus_position = 'header'
'''
if k in ('q', 'Q'):
self.exit()
else:
return
return True
def doformat(self):
walker = ListWalker(get_items())
self.listbox = urwid.ListBox(walker)
btn_q = create_mybutton("Q", self.exit)
header = urwid.GridFlow([btn_q], 6, 1, 1, 'left')
self.footer_text = urwid.Text(u"これはフッター")
footer = urwid.AttrWrap(self.footer_text, "foot")
frame = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'), header=header, footer=footer)
frame.focus_position = 'header'
return frame
def run(self):
self.main_loop = urwid.MainLoop(self.doformat(), my_palette, unhandled_input=self.unhandled_keypress)
self.main_loop.run()
def main():
Application().run()
if __name__=="__main__":
main()
では、実際に動かしてみましょう。
python3 s05_01.py
カーソルがListBoxに移らない....ですね。
これは、headerからbodyへの移動が出来ない状態になっているからです。
def unhandled_keypress(self, k):
'''
if self.main_loop.widget.focus_position == 'header' and k == 'down':
self.main_loop.widget.focus_position = 'body'
elif self.main_loop.widget.focus_position == 'body' and k == 'up':
self.main_loop.widget.focus_position = 'header'
'''
この部分のコメントを削除してみてください。見ればわかると思いますが、headerとbody間での移動を記述しています。
再度実行してみましょう。今度は、ListBox部にFocusが移動しました。
しかし、まだ動きがおかしいですね。今回は、header部からdownキーを押すとListBoxの最初のデータには移動できますが、もう一度downキーを押すと最終行に移動してしまいます。
これは、ListBox内部のWidgetであるTextオブジェクトが、選択可能ではないからです。考えてみると、Textの基本は表示機能ですから、Focusが当たるような処理を想定していないわけで、こうなるのは極めて当然なのです。
ListBox内のTextを選択可能にしよう。
Urwidでは、WidgetがSelectbaleでないとFocusを受け取れないという仕様になっています。可能にするためには、選択可能なTextクラスを作成すればよいのです。
それでは、Textのサブクラスを定義しましょう。ここでは、MyTextクラスとしておきます。このクラスでは、selectableメソッドでTrueを返し、入力を受け取れるようkeypressメソッドを追加、選択可能なWidgetとして定義するわけです。
先程のスクリプトに、MyTextクラスを追加します。また、ListBoxに追加するWidgetを、TextからMyTextに変更しましょう。
# MyTextクラスを追加する
class MyText(urwid.Text):
def selectable(self):
return True
def keypress(self, size, key):
return key
class ListWalker(urwid.ListWalker):
def __init__(self, items):
self.focus = 0
self.create_page(items)
def create_page(self,items):
self.lines = []
for item in items:
# MyTextクラスに変更する。
#text = urwid.Text(item)
text = MyText(item)
self.lines.append(urwid.AttrMap(text, None, 'listwalker'))
これを実行します。
これで、カーソルキーによるデータ選択機能は問題がなくなりました。
しかし、下部にカーソルを移動していくと、画面に表示できなかったデータが、スクロールして出てきてしまいます。
これでもいいのかもしれませんが、TUIでは適切でないインタフェースでしょう。やはり、ここは画面のサイズに適切な行数分だけを表示したいものです。
画面サイズに合わせた表示とデータ選択
それでは、さらに先に進みましょう。
今回のアプリケーションでは、データは100件ほど用意していますが、通常のターミナルは24行が標準です。headerとfooterに1行ほど使用しますから、body部は22行ほどになるわけです。もちろん、決め打ちするのではなく、ターミナルのサイズに従って表示行を決定しないといけません。
画面サイズの取得
画面サイズは、raw_displayのScreenオブジェクトから取得します。
cols, rows = urwid.raw_display.Screen().get_cols_rows()
今回のアプリケーションでは、rowsからheaderとfooterの2行を引いた行数が、bodyの使用できる行数となるわけです。これをスクリプトにくわえます(get_cols_rowsメソッド)。
さらに、データ選択結果の表示を追加します。
ListBox内の行を’enter'キーで選択したときに、取得した内容をfooterに表示してみましょう。select_listというメソッドがそれにあたります。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# s05_03.py(ListBox内に表示できるText件数を考慮する)
#
import urwid
def get_items():
items = []
for i in range(100):
items.append("テストデータ"+str(i))
return items
my_palette = [
('body', 'default', 'default'),
('foot', 'white', 'dark blue'),
('button', 'yellow', 'default'),
('button_focus', 'black', 'yellow'),
('listwalker', 'black', 'light cyan')
]
def create_mybutton(label, callback, user_data=None):
button = urwid.Button(label, on_press=callback)
return urwid.AttrMap(button, 'button', 'button_focus')
class MyText(urwid.Text):
def selectable(self):
return True
def keypress(self, size, key):
if key in ('j', 'J'):
return "down"
elif key in ('k', 'K'):
return "up"
return key
class ListWalker(urwid.ListWalker):
def __init__(self, items, rows):
self.focus = 0
self.create_page(items, rows)
def create_page(self,items, rows):
self.lines = []
self.is_last = False
for i in range(rows):
text = MyText(items[i])
self.lines.append(urwid.AttrMap(text, None, 'listwalker'))
def get_focus(self):
return self.get_at_pos(self.focus)
def set_focus(self, focus):
self.focus = focus
self._modified()
def get_next(self, start):
return self.get_at_pos(start + 1)
def get_prev(self, start):
return self.get_at_pos(start - 1)
def get_at_pos(self, pos):
if pos < 0:
return None, None
if len(self.lines) > pos:
return self.lines[pos], pos
return None, None
class Application:
main_loop = None
def __init__(self):
pass
def exit(self, button=None):
raise urwid.ExitMainLoop()
def get_cols_rows(self):
cols, rows = urwid.raw_display.Screen().get_cols_rows()
return cols, rows-2
def select_list(self):
focus_widget, idx = self.listbox.get_focus()
self.footer_text.set_text("pos:"+str(idx)+" data:"+focus_widget.base_widget.text)
def unhandled_keypress(self, k):
if self.main_loop.widget.focus_position == 'header' and k == 'down':
self.main_loop.widget.focus_position = 'body'
elif self.main_loop.widget.focus_position == 'body' and k == 'up':
self.main_loop.widget.focus_position = 'header'
if k in ('q', 'Q'):
self.exit()
elif k == 'enter':
self.select_list()
else:
return
def doformat(self):
cols, rows = self.get_cols_rows()
walker = ListWalker(get_items(), rows)
self.listbox = urwid.ListBox(walker)
btn_q = create_mybutton("Q", self.exit)
header = urwid.GridFlow([btn_q], 6, 1, 1, 'left')
self.footer_text = urwid.Text(u"これはフッター")
footer = urwid.AttrWrap(self.footer_text, "foot")
frame = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'), header=header, footer=footer)
frame.focus_position = 'header'
return frame
def run(self):
urwid.set_encoding('UTF-8')
self.main_loop = urwid.MainLoop(self.doformat(), my_palette, unhandled_input=self.unhandled_keypress)
self.main_loop.run()
def main():
Application().run()
if __name__=="__main__":
main()
では、実際に動かしてみましょう。
python3 s05_03.py
これで、想定した動きになりました。
矢印キーでListBox内を移動できますし、Enterキーを叩くと、footer部に選択されたデータが表示されています。
しかし、画面上には22行のデータだけが表示されています。実際のデータは100件ほどあるわけですから、表示されないデータがあるのは困りものです。
次回はページング処理を入れて、この部分を追加していきましょう。