PythonでTUIアプリを作ろう ( その6 ListBoxの画面コントロール )
ページング処理を追加しよう。
今回は、前回のスクリプトにページコントロールを入れて、全件表示ができるよう変更していきます。新たなWidgetの追加はありません。処理は極めて簡単で、ページング対応ロジックの追加が中心になります。
画面イメージ
まずは、最終画面イメージを示します。
Header部に、ページングボタンを2つ配置しています。このボタンがクリックされることで、ページング表示を行っていくという形となるわけです。
スクリプトの分割
その前にスクリプトを整理しましょう。
今までは、1つのスクリプトに全ての処理を記述していましたが、だいぶ長くなってきたので、下記の3ファイルに分割し、ページングに対応した処理を追加していきます。
(1) Widget関係(mywidget.py)
(2) ListWalker(listwalker.py)
(3) メイン部(list.py)
それでは、個々のスクリプトを見ていきましょう。
(1) mywidget
Urwidの基本Widgetを拡張している部分を抜き出します。
今回は新たに、「マウスのダブルクリック」に対応したMyFrameを追加しました。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# mywidget.py
#
import urwid
import time
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 MyFrame(urwid.Frame):
last_time_clicked = None
double_click = None
def mouse_event(self, size, event, button, col, row, focus):
if event == 'mouse press':
now = time.time()
if (self.last_time_clicked and (now - self.last_time_clicked < 0.5)):
if self.double_click:
self.double_click()
else:
urwid.Frame.mouse_event(self, size, event, button, col, row, focus)
self.last_time_clicked = now
(2) listwalker
処理件数(rows)にだけではなく、表示開始レコード(from_rec)をパラメータに追加しています。また、最終ページを示すフラグ(is_last)をプロパティとして設定しています。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# listwalker.py
#
import urwid
from mywidget import MyText
class ListWalker(urwid.ListWalker):
def __init__(self, items, from_rec, rows):
self.focus = 0
self.create_page(items, from_rec, rows)
def create_page(self,items, from_rec, rows):
self.lines = []
self.is_last = False
for i in range(rows):
if len(items) < from_rec + i:
self.is_last = True
break
text = MyText(items[from_rec+i-1])
self.lines.append(urwid.AttrMap(text, None, 'listwalker'))
def last_page(self):
return self.is_last
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
(3) list
これまでは、Applicationという抽象的なクラス名にしていましたが、一覧処理なので、Listという名称に変更しました。
1. ページングボタンの追加
Headerにページングボタンを追加します。<N>ボタンで次ページ、<P>ボタンで前ページ画面に移行することとします。
2. ボタンの処理
表示開始レコード(from_rec)を、ページングボタンの動作に合わせて増減することで画面ページングを行います。
また、1ページめは<P>ボタンを、ListWalkerが最終ページフラグ(is_last)を返した場合は<N>ボタンの表示を抑制するよう制御します。
下記にコードを示します。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# list.py
#
import urwid
from listwalker import ListWalker
from mywidget import *
class List:
main_loop = None
from_rec = 1
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 next_page(self, ignored=None):
if not self.last_page():
self.from_rec += self.rows
self.main_loop.widget = self.doformat()
def prior_page(self, ignored=None):
if self.from_rec > self.rows:
self.from_rec -= self.rows
self.main_loop.widget = self.doformat()
def first_page(self):
return self.from_rec == 1
def last_page(self):
return self.is_last
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 in ('n', 'N'):
self.next_page()
elif k in ('p', 'P'):
self.prior_page()
elif k == 'enter':
self.select_list()
else:
return
return True
def doformat(self):
self.cols, self.rows = self.get_cols_rows()
walker = ListWalker(get_items(), self.from_rec, self.rows)
self.is_last = walker.last_page()
self.listbox = urwid.ListBox(walker)
if self.last_page():
btn_next = urwid.Divider()
else:
btn_next = create_mybutton("N", self.next_page)
if self.first_page():
btn_prior = urwid.Divider()
else:
btn_prior = create_mybutton("P", self.prior_page)
btn_q = create_mybutton("Q", self.exit)
header = urwid.GridFlow([btn_next, btn_prior, 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 = MyFrame(urwid.AttrWrap(self.listbox, 'body'), header=header, footer=footer)
frame.double_click = self.select_list
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():
List().run()
if __name__=="__main__":
main()
動作確認
では、実際に動かしてみましょう。mywidget.py、listwalker.py、list.pyの3ファイルを同一ディレクトリに入れておきます。
python3 list.py
先に示した画面が表示されたでしょうか。
<N>ボタン、<P>ボタンでページング処理が行われます。また、データをマウスでダブルクリックすると、選択されたレコードの内容がfotter部に表示されることも確認してください。
これで、Urwid ListBoxを使用した一覧リスト画面(List)の雛形は完成です。
次回は、この画面から選択されたレコードの詳細(Detail)を表示するスクリプトを作成していきます。Urwidのようなツールキットで開発する場合、複数画面連携は大きなポイントとなります。