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.py、mywidget.py、listwalker.py、list.py、Detail.py、common.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を追加してみましょう。