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を使って出すという仕様です。

まずは、標準的なMessageBoxmy_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.pymywidget.pylistwalker.pylist.pyDetail.pycommon.pyの6ファイルを、同一ディレクトリに入れておきます。

python3 list.py  

DialogBoxによる操作を確認してみてください。


とりあえず、ここまで。

ここまで数回に渡って、Python Urwidの概要と主なWidgetを説明してきました。紹介した部品を活用することで、TUIアプリケーション開発は可能になると思います。
本来なら、もっと臨場感のあるデータを使って説明したかったのですが、色々支障もあって、味気ない内容になってしまいました。機会を見て、新たな内容を追加していきたいと考えています。

(2021/10追記)
第二部入魂編を作成し始めました。


ソースコードについて

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