PythonでTUIアプリを作ろう ( その9 DialogBoxの利用 )
UrwidのOverlayをマスターしよう。
アプリケーション画面設計を行う際には、データを選択したり、条件入力を行う機能が不可欠です。一般にそのような補助入力画面は、DialogBoxという形式で実装されています。
IT用語辞典では、
ダイアログボックスとは、コンピュータの操作画面で、利用者に何らかの入力を促すために表示される矩形の領域のこと。小さなウィンドウの形で表示されることが多い。“dialog” は「対話」の意。
と定義されています。
今回は、今まで作成してきたスクリプトに、DialogBoxによる入力機能を導入していきましょう。多くのUIツールキットでは、DialogBoxを機能別に分類した上で、標準ツールとして提供されていますが、残念ながらPython Urwidでは、基本的な枠組みが提供されているだけで、個々の機能はそれぞれ独自に実装しないといけません。
UrwidのOverlayについて
Urwidにおいて、DialogBoxを実現するベースとなるのは、Overlayという機能です。これはその名のとおり、表示されている画面上に入力画面を重ねて表示、ユーザーからの指示を受け取る形態をサポートします。
Overlayクラスの構造は、下記となります。
class urwid.Overlay(top_w, bottom_w, align, width, valign, height, min_width=None, min_height=None, left=0, right=0, top=0, bottom=0)
重要なのは、最初の2つの引数、top_w、とbottom_wです。この2つはContainer widgetであり、bottom_wの上にtop_wが重ねて(Overlay)表示されることになります。残りのパラメータは配置指定ですので、テストをしながら適当に決めていけばよいでしょう。
DialogBoxを実装してみる。
それでは、実際に画面とコードを対比しながら、説明していきましょう。
Overlay構造を画面に表示する、my_dialogというメソッドを作ってみました。(mywidget.py)
def my_dialog(main_loop, overlay, align="center", valign="middle", width=30, height=15, min_width=0, min_height=0):
main_loop.widget = urwid.Overlay(overlay, main_loop.widget, align=align, valign=valign, width=width, height=height)
下部に当たるWidgetは、現在表示されている画面のコンテナmain_loop.widget、上部にオーバーレイ表示するWidget(overlayパラメータ)という構成です。
この状態をurwid.Overlayメソッドで生成し、最終コンテナとしてmain_loop.widgetにセットし直すという手順になります。後の位置決めパラメータは、適当に設定すればよいでしょう。
これは、オバーレイ表示の最終型ですので、その前にニーズに合わせて、overlay widgetを生成していくという手順を踏む必要があります。
(1) SelectBox
最初のDialogBoxは、リスト表示されたデータの中から1項目を選択するという形式です。SelectBoxという名称にしておきましょう。画面イメージは下記となります。
今回は、ListクラスのHeader部に、<T>ボタンを配置し、このボタンが押されたら、SelectBoxが表示されるという形としました。
まず、SelectBoxの生成コードを記述します。(mywidget.py)
def my_select_box(main_loop, entries, current, title, callback):
body = []
selected = -1
for i, entry in enumerate(entries):
if entry == current:
selected = i
button = MyButton(entry, callback)
body.append(urwid.AttrMap(button, None, 'button_focus'))
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(body))
if selected >= 0:
listbox.focus_position = selected
my_dialog(main_loop, urwid.LineBox(listbox, title=title))
リスト構造内の要素として、ボタンを配置します。ボタン生成事の引数には、callbackが指定できるので、項目がセレクトされた際のイベントを設定しておきます。
このmy_selectboxを使用するスクリプトは、下記のようになります。(list.py)
リストを構成する要素、現在使われている要素、タイトル名、そして選択された項目を処理するcallbackメソッドを指定しておきます。
:
:
tables = u'テーブルA テーブルB テーブルC テーブルE テーブルF テーブルG テーブルH テーブルI テーブルJ テーブルK テーブルL テーブルM'.split()
my_select_box(self.main_loop, tables, self.common.table_name, u"Tables", self.get_table_name)
def get_table_name(self, button):
self.common.table_name = button.get_label()
self.display()
リスト構造から選択されたボタン上のTextが、共通オブジェクト内にセットされ、画面が一新するという構成になります。今回は、選択されたテーブル名に合わせて、Listクラスの表示項目も更新されるよう、コードを変更しております。
(2) InputBox
次に、検索条件など文字列を入力する画面を作成していきます。InputBoxという名称にしておきましょう。下記のような画面イメージとなります。
入力フィールド(Edit)の下部に、”OK”と”Cancel"のボタンを配置した画面をoverlay widgetとして生成、my_dialogに渡します。
その7で説明しましたように、Pileを画面表示するには、Fillerなどでラップしないといけません。さらに、今回は画面枠設定のため、LineBoxで囲んでおきます。
:
:
edit = urwid.Edit("", align="left")
btn_OK = create_mybutton("OK", self.on_submit, edit)
btn_Cancel = create_mybutton("Cancel", self.on_submit, edit)
ok_cancel = urwid.GridFlow([btn_OK, btn_Cancel], 10, 1, 1, 'right')
pile = [
urwid.AttrMap(edit, "search", "search"),
urwid.Divider(),
urwid.Padding(ok_cancel)
]
my_dialog(self.main_loop, urwid.LineBox(urwid.Filler(urwid.Pile(pile)), title=u'Search', title_align='left'), height=7, align=('relative', 40), valign=('relative', 10))
def on_submit(self, button, edit):
self.display()
self.footer_text.set_text(button.get_label()+" "+edit.get_edit_text())
通常処理ならば、入力された条件で画面を更新するのでしょうが、今回はダミーデータのため画面を戻した後、入力データと押されたボタンの種類をFotterに表示するにとどめておきます。
(3) MessageBox
データの更新や削除を行う場合は、確認メッセージを表示することが一般的です。このようなメッセージを表示するMessageBoxも作成しておきましょう。画面イメージは下記となります。
DetailクラスのFotter部に、<U>ボタンと、<D>ボタンを配置しています。これは、更新(Update)、削除(Delete)を想定している処理となります。このボタンを押された場合、実際の更新、削除処理を行う前に、確認画面をMessageBoxを使って出すという仕様です。
まずは、標準的なMessageBoxをmy_messageboxとして定義しておきます。(mywidget.py)
def my_message_box(main_loop, title, callback):
btn_OK = create_mybutton("OK", callback)
btn_Cancel = create_mybutton("Cancel", callback)
ok_cancel = urwid.GridFlow([btn_OK, btn_Cancel], 10, 1, 1, 'center')
pile = [
urwid.Divider(),
urwid.Padding(ok_cancel)
]
my_dialog(main_loop, urwid.LineBox(urwid.Filler(urwid.Pile(pile)), title=title, title_align='left'), height=7, align=('relative', 40), valign=('relative', 10))
<U>ボタンでupdateメソッド、<D>ボタンでdeleteメソッドが呼ばれます。コードは下記のようになります。(detail.py)
def update(self, ignored=None):
my_message_box(self.main_loop, u'Update record ?', self.go_update)
def delete(self, ignored=None):
my_message_box(self.main_loop, u'Delete record ?', self.go_delete)
def go_update(self, button):
if button.get_label() == 'OK':
self.items[self.common.selected_item-1][0] = self.edit_field01.base_widget.get_edit_text()
self.items[self.common.selected_item-1][1] = self.edit_field02.base_widget.get_edit_text()
self.items[self.common.selected_item-1][2] = self.edit_note.base_widget.get_edit_text()
self.display()
def go_delete(self, button):
if button.get_label() == 'OK':
self.return_main()
else:
self.display()
今回はデータがダミーなので、更新、削除処理は出来ません。一応配列の更新を入れていますが、この画面を離れると元に戻ってしまいます。
動作確認
では、実際に動かしてみましょう。application.py、mywidget.py、listwalker.py、list.py、Detail.py、common.pyの6ファイルを、同一ディレクトリに入れておきます。
python3 list.py
DialogBoxによる操作を確認してみてください。
とりあえず、ここまで。
ここまで数回に渡って、Python Urwidの概要と主なWidgetを説明してきました。紹介した部品を活用することで、TUIアプリケーション開発は可能になると思います。
本来なら、もっと臨場感のあるデータを使って説明したかったのですが、色々支障もあって、味気ない内容になってしまいました。機会を見て、新たな内容を追加していきたいと考えています。
(2021/10追記)
第二部入魂編を作成し始めました。