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.pylistwalker.pylist.pyの3ファイルを同一ディレクトリに入れておきます。

python3 list.py  

先に示した画面が表示されたでしょうか。
<N>ボタン、<P>ボタンでページング処理が行われます。また、データをマウスでダブルクリックすると、選択されたレコードの内容がfotter部に表示されることも確認してください。

これで、Urwid ListBoxを使用した一覧リスト画面(List)の雛形は完成です。
次回は、この画面から選択されたレコードの詳細(Detail)を表示するスクリプトを作成していきます。Urwidのようなツールキットで開発する場合、複数画面連携は大きなポイントとなります。


ソースコードについて

GitHubに登録しました。今回のコードは、Section06となります。