PythonでTUIアプリを作ろう ( その8 画面間データ連携について )

データ連携用の共通オブジェクトを設定する。


今回は、前回定義したListクラスとDetailクラス間に、データ連携機能を組み込んでいきます。と言っても難しい話ではなく、オブジェクトを定義して起動時に引き渡すだけのことです。

今回想定している動きは、下記のようになります。

Listでデータを選択すると、該当のデータがDetailに表示される。それだけですね。


事前処理

メインロジックを変更していく前に、必要な事前処理を行います。

(1) データの準備

前準備として、連携ができるようデータアイテムを調整しておきます。mywidget.py内のget_itemメソッドを下記のように変更します。フィールド項目を3つに増やし、最初のフィールドで個別データを識別できるようにしておきます。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# mywidget.py  
#  
import urwid  
import time  
  
def get_items(name):  
    items = []  
    for i in range(100):  
        item = []  
        item.append(name+str(i))  
        item.append(name+str(i)+" フィールド02")  
        item.append("改行を含む入力データ\n2行目のデータ\n3行目のデータ")  
        items.append(item)  
    return items  
  

(2) ListWalkerの変更

フィールドが3つにふえたので、リスト表示を行っているlistwalker.pyも最初のフィールドを表示するよう変更します。

#!/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])  
            text = MyText(items[from_rec+i-1][0])  
            self.lines.append(urwid.AttrMap(text, None, 'listwalker'))  

これで、データの準備は完了です。


共通クラスの定義

次に、新しいクラスを定義します。
ListクラスとDetailクラス間でやり取りをされる共通データですから、Commonクラスと言う名称にしましょう。
とりあえずは、開始レコード(from_rec)と選択レコード(selected_item)を項目として入れておきます。table_nameは、後の拡張用なのでとりあえず無視してください。
下記のコードを、common.pyとして用意します。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# common.py  
#  
class Common:  
    table_name = "テータ"  
    from_rec = 1  
    selected_item = 1  

既存クラスの変更

ここからは、既存のクラスに変更を加えていきます。やることは単純、共通オブジェクトCommonを受け取れるようにするだけです。順に見ていきましょう。


(1) Applicationクラス

クラス間を連携するstartメソッドは、親クラスであるApplicationクラス内に定義されているので、ここに共通オブジェクトCommonをパラメータとして追加します。

    def start(self, next_class, common=None):  
        next_class(common).run(self.main_loop)  

こんなコードに変更します。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# application.py  
#  
import urwid  
from mywidget import my_palette  
  
class Application:  
    main_loop = None  
  
    '''  
    @classmethod  
    def get_instance(cls, common=None):  
        if not hasattr(cls, "_instance"):  
            cls._instance = cls(common)  
        else:  
            cls._instance.common = common  
        return cls._instance  
    '''  
    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 unhandled_keypress(self, k):  
        return True  
  
    def doformat(self):  
        return urwid.widget  
  
    def display(self):  
        self.main_loop.widget = self.doformat()  
  
    def start(self, next_class, common=None):  
        next_class(common).run(self.main_loop)  
        #next_class.get_instance(common).run(self.main_loop)  
  
    def run(self, main_loop=None):  
        if main_loop != None:  
            self.main_loop = main_loop  
            self.main_loop.unhandled_input = self.unhandled_keypress  
            self.display()  
        else:  
            self.main_loop = urwid.MainLoop(self.doformat(), my_palette, unhandled_input=self.unhandled_keypress)  
            self.main_loop.run()  

(2) Listクラス

各画面クラスでは、コンストラクタにて共通オブジェクトCommonを受け取ることになりますので、そのコードを追加します。

class List(application.Application):  
    def __init__(self, common):  
        self.common = common  

また、Listから、Detailを呼び出す部分は、選択されたレコード番号を引き渡すロジックを追加しておきましょう。

    def start_detail(self):  
        self.common.selected_item = self.listbox.focus_position + self.common.from_rec  
        self.start(detail.Detail, self.common)  

コード全体を示します。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# list.py  
#  
import urwid  
  
import application  
import detail  
from listwalker import ListWalker  
from mywidget import *  
from common import Common  
  
class List(application.Application):  
    def __init__(self, common):  
        self.common = common  
  
    def next_page(self, ignored=None):  
        if not self.last_page():  
            self.common.from_rec += self.rows  
            self.main_loop.widget = self.doformat()  
  
    def prior_page(self, ignored=None):  
        if self.common.from_rec > self.rows:  
            self.common.from_rec -= self.rows  
            self.main_loop.widget = self.doformat()  
  
    def first_page(self):  
        return self.common.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.start_detail()  
        else:  
            return  
        return True  
    def get_start_record(self, item):  
        if self.common.from_rec >= self.common.selected_item:  
            return self.common.from_rec  
        if int(item/self.rows)*self.rows == item:  
            return (int(item/self.rows) - 1)*self.rows+1  
        else:  
            return (int(item/self.rows))*self.rows+1  
    def doformat(self):  
        self.cols, self.rows = self.get_cols_rows()  
        self.common.from_rec =  self.get_start_record(self.common.selected_item)  
  
        walker = ListWalker(get_items(self.common.table_name), self.common.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 = MyFrame(urwid.AttrWrap(self.listbox, 'body'), header=header, footer=footer)  
        frame.double_click = self.start_detail  
  
        if self.common.from_rec <= self.common.selected_item < self.common.from_rec+self.rows:  
            self.listbox.focus_position = self.common.selected_item - self.common.from_rec  
            self.common.selected_item = 0  
            frame.focus_position = 'body'  
        else:  
            frame.focus_position = 'header'  
        return frame  
  
    def start_detail(self):  
        self.common.selected_item = self.listbox.focus_position + self.common.from_rec  
        self.start(detail.Detail, self.common)  
  
def main():  
    common = Common()  
    List(common).run()  
  
if __name__=="__main__":  
    main()  
  

(3) Detailクラス

Detailから、Listに戻る場合は、特に何もしません。共通オブジェクトCommonをを、そのまま返すだけです。

    def return_main(self, ignored=None):  
        self.start(list.List, self.common)  

コード全体を示します。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# list.py  
#  
import urwid  
  
import application  
import list  
from mywidget import *  
from common import Common  
  
class Detail(application.Application):  
    def __init__(self, common):  
        self.common = common  
        self.is_last = False  
  
    def next_page(self, ignored=None):  
        if self.is_last == False:  
            self.common.selected_item += 1  
            self.display()  
  
    def prior_page(self, ignored=None):  
        if self.common.selected_item > 1:  
            self.common.selected_item -= 1  
            self.display()  
  
    def first_page(self):  
        return self.common.selected_item == 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 ('r', 'R'):  
            self.return_main()  
        elif k in ('n', 'N'):  
            self.next_page()  
        elif k in ('p', 'P'):  
            self.prior_page()  
        else:  
            return  
        return True  
  
    def doformat(self, from_rec=1):  
        items = get_items(self.common.table_name)  
        self.edit_field01 = create_myedit(items[self.common.selected_item-1][0])  
        self.edit_field02 = create_myedit(items[self.common.selected_item-1][1])  
        self.edit_note = create_myedit(items[self.common.selected_item-1][2],multiline=True)  
        pile = [  
            create_mylabel("Field01"),  
            self.edit_field01,  
            create_mylabel("Field02"),  
            self.edit_field02,  
            create_mylabel("Note"),  
            self.edit_note  
        ]  
        body = urwid.Filler(urwid.Pile(pile), valign='top')  
        if self.common.selected_item == len(items):  
            self.is_last = True  
  
        btn_return = create_mybutton("R", self.return_main)  
        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_exit = create_mybutton("Q", self.exit)  
        header = urwid.GridFlow([btn_return, btn_next, btn_prior, urwid.Divider(  
), urwid.Divider(), btn_exit], 6, 1, 1, 'left')  
  
        self.footer_text = urwid.Text(u"これはフッター")  
        footer = urwid.AttrWrap(self.footer_text, "foot")  
  
        frame = MyFrame(urwid.AttrWrap(body, 'body'), header=header, footer=footer)  
        frame.focus_position = 'header'  
        return frame  
  
    def return_main(self, ignored=None):  
        self.start(list.List, self.common)  
  
def main():  
    common = Common()  
    Detail(common).run()  
  
if __name__=="__main__":  
    main()  

動作確認

では、実際に動かしてみましょう。application.pymywidget.pylistwalker.pylist.pyDetail.pycommon.pyの6ファイルを、同一ディレクトリに入れておきます。

python3 list.py  

先に示した画面が表示されたでしょうか。


クラスをSingletonに変更する

最後に少し修正しておきます。
ここまでのスクリプトでは、ListクラスとDetailクラスを行ったり来たりするたびに、新しいクラスが生成されてしまいます。これは資源の無駄使いなので、Singleton構造にして、1回限りの生成にしておきます。Singletonというのはデザインパターンの一つですので、詳細は適当なサイトを検索してみてください。

下記の様に、get_instanceメソッドを追加し、startメソッドを変更します。

class Application:  
    main_loop = None  
  
    @classmethod  
    def get_instance(cls, common=None):  
        if not hasattr(cls, "_instance"):  
            cls._instance = cls(common)  
        else:  
            cls._instance.common = common  
        return cls._instance  
  
    :  
    :  
  
    def start(self, next_class, common=None):  
        #next_class(common).run(self.main_loop)  
        next_class.get_instance(common).run(self.main_loop)  

画面連携処理はこれで終了です。次回は、DialogBoxのようなWidgetを追加してみましょう。


ソースコードについて

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