PythonでTUIアプリを作ろう ( その3 Python Urwidの基本オブジェクト構造 )

UrwidのMainLoop、Widget、Eventを理解しよう。


Urwidの基本機能を理解するには、まず、UrwidのWebサイトにアクセスし、ドキュメンテーションを見ることが第一歩でしょう。

最初に”Tutorial”にあるスクリプトを動かして、Urwidの基本的な動きや、画面表示について感触を掴みます。これを軽く終了した後、Manualに目を通していくというのが、最初のアプローチになると思います。

ここでは、その一助となるよう、少し補足を加えていきましょう。


基本構造を理解する

ほとんどのUIアプリケーションでは、「メッセージループを生成し、そこに生じるイベントを処理すること」が、プログラムの基本動作になりますが、Urwidもその例に漏れません。まずは、Urwidにおける登場人物ならぬ、登場オブジェクトを把握していきます。

(1) MainLoop

まずは、MainLoop。これがすべての中心となります。UrwidのManualには、下記のように図示されています。

MainLoopクラスは、Display ModuleWidgetEventとの仲介をする中心クラスです。Display Moduleから入力されたインプットをWidgetに伝えたり、画面レンダリングをすることなど、ほとんどの機能はこのクラスを経由して実行されます。

(2) Widget

次に、Widget
Widgetは、画面を構成する部品です。Widgetは、層構造を取ることができ、あるwidget上に、別のWidgetが乗るような部品構造を取ることが一般的です。

上の図では、Widget “a"上に、”b”、さらに”c”、”d”、”e”というような層構造を示しています。
一般に、最下層でベースとなる部品をコンテナと呼びますが、この図ではWidget “a"がコンテナにあたります。

(3) Event

ユーザーによってキー入力などの処理が行われると、それに対応するEventが発生します。Eventは、それぞれのWidgetに伝えられていくことになります。(2)で示した構造では、“a"から順番にEventが伝搬されていきます。


実例で理解する

具体的に、Tutorialでも取り上がられている下記のスクリプトを例にしましょう。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# s03_01.py  
#  
import urwid  
  
def show_or_exit(key):  
    if key in ('q', 'Q'):  
        raise urwid.ExitMainLoop()  
    txt.set_text(repr(key))  
  
txt = urwid.Text(u"Hello World")  
fill = urwid.Filler(txt, 'top')  
loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)  
loop.run()  

このスクリプトは、

  • 画面の最上行に"Hello World"と表示後、キー入力を待つ。
  • キーが”q”か”Q”の場合、終了。
     他のキーであれば、入力キーを最上行に表示。

というものです。

ここでは、最後の3行で、Widgetと、MainLoopを定義しています。
まず、Widgetですが、今回はテキスト一行のみです。Fillerについては、ここでは置いておきましょう。これをMainLoopにセットすることで、画面が作成されメッセージループに入るということになります。

unhandled_inputメソッド

つぎにEventですが、今回は表示されているText Widgetは表示機能なので、入力に対応するメソッドを持っていません。
Urwidでは、

Widgetが扱わないイベントは、最終的にunhandled_inputメソッドに渡る」

という構造になっているので、必要な処理はここに記述することになるでしょう。なお、このメソッドは事前にMainLoopクラスに設定しておく必要があります。

スクリプトの終了方法

さて、このスクリプトを終了するためには、最終行でセットしたメッセージループを抜けなければなりません。
Urwidでは、どこでも良いので、urwid.ExitMainLoop()という例外をraiseします。これにより、スクリプトが終了することになります。


オブジェクト概念図

以上の基本オブジェクト構造を、概念図にしてみました。

まとめると、

  • Widgetは層構造で、その最終レベルはMainLoopと1:1に対応。(このWidgetは、MainLoopのwidgetプロパティとしてアクセスすることができる。)
  • Eventは、まず上記のWidgetに渡される。その後、次のWidgetへと伝搬されていく。
  • Widgetの処理対象から外れたEventは、unhandled inputメソッドが定義されていれば、そこで処理対象となる。

ということになります。


クラス化する

今回のスクリプトを、今後の展開のベースとなるようクラス化しておきます。ここでは、Applicationクラスとして実装しました。
また、最終のWidgetをFrameに変更します。Frameは、上部からHeader、Body、Footerと3つの部分から構成されるコンテナです。HeaderとFooterはなくても良いので、どのような形にでも適用できる一般的なものとなります。また、小さなことですが、表示位置も画面中央に変更しておきます。

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
#  
# s03_02.py  
#  
import urwid  
  
class Application:  
    main_loop = None  
    def __init__(self):  
        pass  
  
    def exit(self):  
        raise urwid.ExitMainLoop()  
  
    def unhandled_keypress(self, k):  
        if k in ('q', 'Q'):  
            self.exit()  
        self.text.set_text(repr(k))  
  
    def doformat(self):  
        self.text = urwid.Text(u"Hello World")  
        filler = urwid.Filler(self.text, 'middle')  
        frame = urwid.Frame(filler)  
        return frame  
  
    def run(self):  
        self.main_loop = urwid.MainLoop(self.doformat(), unhandled_input=self.unhandled_keypress)  
        self.main_loop.run()  
  
def main():  
    Application().run()  
  
if __name__=="__main__":  
    main()  
  

次回は、このクラスをベースに、新たなWidgetを追加していきましょう。


ソースコードについて

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