Python で Curses プログラミング

curses ってなに?

curses ライブラリは、VT100s や Linux コンソール、さまざまなプログラムが提供するエミュレーション端末といったテキストベースの端末(ターミナル)のために、端末に依存しないスクリーン描画や、キーボードの処理を提供します。端末はカーソルの移動や、画面のスクロール、領域の消去といった共通の操作を行うための様々な制御コードをサポートします。端末の種類によって大きく異なる制御コードを使うことがあり、しばしば独特の癖があります。

グラフィカルなディスプレイが当たり前の今となっては、「なんでわざわざ?」と疑問に思うかもしれません。確かに文字表示端末は時代遅れな技術ではありますが、ニッチな領域が存在していて、意匠を凝らすことができるため、いまだに価値のあるものとなっています。そのような領域の一つは、例えば X サーバーを持たない組み込みのUnixです。ほかにも、OSのインストーラや、カーネル設定などのツールで、これらはグラフィカルサポートが利用可能になる前に動作する必要があります。

curses ライブラリが提供するのは比較的基本的な機能です。 画面を、互いに重ならない複数のテキストウィンドウとして扱う仕組みを提供します。 ウィンドウの内容は、テキストの追加や削除、見た目の変更など、さまざまな方法で変更できます。 curses ライブラリは、意図した表示にするために端末へ送る制御コードを判断します。 ボタン、チェックボックス、ダイアログなどのユーザーインターフェース部品の多くは提供しません。 そのような機能が必要なら、Urwid などのユーザーインターフェースライブラリを検討してください。

curses ライブラリは元々 BSD UNIX 向けに書かれました; 後の AT&T から出た Unix System V バージョンで多くの機能と新機能が追加されました。BSD curses はいまやメンテナンスされておらず、これは AT&T インターフェースのオープンソース実装である ncurses にとって置き換えられました。Linux や FreeBSD のようなオープンソース Unix を利用している場合は、おそらくシステムは ncurses を利用しています。現在のほとんどの商用 Unix は System V のコードを基にしているため、ここで述べる全ての関数が利用できるはずです。しかし、古いバージョンの curses を持ついくつかのプロプライエタリ Unix は全てには対応していないでしょう。

Windows 版の Python には curses モジュールが含まれていません。 サードパーティーの windows-curses パッケージが、Windows で同じインターフェースを提供します。

Python の curses module

Python のモジュールは、curses が提供する C 関数の比較的単純なラッパです。 C での curses プログラミングに慣れていれば、その知識を Python でも簡単に生かせます。 最大の違いは、Python のインターフェースでは addstr()、mvaddstr()、mvwaddstr() のような別々の C 関数を 1 つの addstr() メソッドにまとめ、扱いやすくしていることです。 詳しくは後で説明します。

この HOWTO は curses と Python を使ってテキストプログラムを書くための入門記事です。 curses API に対する完全な解説をすることは意図していません; その目的のためには Python ライブラリガイドの ncurses 節と C 言語マニュアルのncurses のページを参照してください。とはいえ、この文章は基本的な考えを提供してくれるでしょう。

curses アプリケーションの起動と終了

何をするにしても、まず curses を初期化する必要があります。 initscr() 関数を呼び出すと、端末の種類を判定し、必要な設定コードを端末へ送り、内部の各種データ構造を作成します。 成功すると、initscr() は画面全体を表すウィンドウオブジェクトを返します。 これは対応する C の変数名にちなんで、通常 stdscr と呼ばれます。

import curses
stdscr = curses.initscr()

通常、curses アプリケーションではキー入力を自動的に画面へ表示する機能 (エコー) を無効にし、キーを読み取った後、必要な場合にだけ表示します。 そのためには noecho() 関数を呼び出します。

curses.noecho()

通常アプリケーションはまた、Enter キーを押すことなく、キーに対してすぐに反応する必要があります; これは cbreak モードと呼ばれ、通常の入力がバッファされるモードと逆に動作します。

curses.cbreak()

端末は通常、カーソルキーや Page Up、Home などの特殊キーを、複数バイトのエスケープシーケンスとして返します。 これらのシーケンスを自分で処理することもできますが、curses に任せると curses.KEY_LEFT のような特別な値として受け取れます。 そのためには、キーパッドモードを有効にします。

stdscr.keypad(True)

curses アプリケーションを終了させるのは起動よりも簡単です。 以下を呼び出す必要があります:

curses.nocbreak()
stdscr.keypad(False)
curses.echo()

cursesに親和性の高い設定をもとに戻します。そして、endwin() 関数を呼び出し、端末を通常の操作モードに復旧します。

curses.endwin()

curses アプリケーションのデバッグでよくある問題は、端末の状態を復元する前にアプリケーションが終了し、端末がおかしな状態のままになることです。 Python では、コードのバグで例外が発生し、それが捕捉されなかった場合によく起こります。 例えば、入力したキーが画面に表示されなくなり、シェルを使いにくくなります。

Pythonでは、これらの複雑さを避けデバッグをより容易にするために、 curses.wrapper() 関数をインポートして、このように使います:

from curses import wrapper

def main(stdscr):
    # 画面を消去する
    stdscr.clear()

    # i == 10 のとき ZeroDivisionError が発生する。
    for i in range(0, 11):
        v = i-10
        stdscr.addstr(i, 0, '10 divided by {} is {}'.format(v, 10/v))

        stdscr.refresh()
        stdscr.getkey()

wrapper(main)

wrapper() 関数は呼び出し可能オブジェクトを受け取り、上記の初期化を行います。 色がサポートされていれば、色の初期化も行います。 その後、wrapper() は渡された呼び出し可能オブジェクトを実行します。 その呼び出しから戻ると、wrapper() は端末をもとの状態に戻します。 呼び出しは try...except の中で行われ、例外が発生すると、それを捕捉して端末の状態を復元してから再送出します。 そのため、例外が発生しても端末が異常な状態のままにならず、例外のメッセージやトレースバックを読めます。

ウィンドウとパッド

curses の基本となる抽象化はウィンドウです。 ウィンドウオブジェクトは画面上の長方形の領域を表し、テキストの表示や消去、利用者による文字列の入力などのメソッドを備えています。

initscr() 関数が返す stdscr オブジェクトは、画面全体を覆うウィンドウオブジェクトです。 多くのプログラムはこのウィンドウだけで十分ですが、個別に再描画や消去を行うために、画面を小さなウィンドウに分けたい場合もあるでしょう。 newwin() 関数は、指定されたサイズの新しいウィンドウを作り、そのウィンドウオブジェクトを返します。

begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

curses の座標系は独特なので注意してください。 座標は常に y,x の順に渡し、ウィンドウの左上隅が (0,0) です。 これは x 座標を先にする通常の慣習と異なります。 ほかの多くのアプリケーションと違うのは残念ですが、curses の最初の実装からこの方式なので、今から変更するには遅すぎます。

画面のサイズは、curses.LINES と curses.COLS 変数からそれぞれ y 方向と x 方向の大きさを取得して調べられます。 有効な座標の範囲は (0,0) から (curses.LINES - 1, curses.COLS - 1) までです。

テキストを表示または消去するメソッドを呼び出しても、その結果はすぐには画面に現れません。 画面を更新するには、ウィンドウオブジェクトの refresh() メソッドを呼び出す必要があります。

これは、curses が当初、300 ボーという低速な端末接続を想定して書かれたためです。 そのような端末では、画面の再描画にかかる時間を最小限にすることが非常に重要でした。 そこで curses は画面への変更を蓄積し、refresh() が呼ばれたときに最も効率的な方法で表示します。 例えば、プログラムがウィンドウにテキストを表示してからそのウィンドウを消去した場合、テキストは実際には表示されないので、端末へ送る必要がありません。

実際には、ウィンドウの再描画を明示的に指示しても、curses のプログラミングはそれほど複雑になりません。 多くのプログラムは一連の処理を行った後、キー入力などの利用者の操作を待って休止します。 入力待ちに入る前に stdscr.refresh() または必要なほかのウィンドウの refresh() を呼び出し、画面を再描画しておけばよいのです。

パッドは特殊なウィンドウです。 実際の画面より大きくでき、一度に表示するのはその一部だけです。 作成時にはパッドの高さと幅を指定し、更新時にはパッドの一部を表示する画面上の領域の座標を指定します。

pad = curses.newpad(100, 100)
# このループでパッドを文字で埋める。addch() は
# 次の節で説明する。
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# 画面中央にパッドの一部を表示する。
# (0,0) : 表示するパッド領域の左上隅の座標。
# (5,5) : パッドの内容を表示するウィンドウ領域の
#         左上隅の座標。
# (20, 75) : パッドの内容を表示するウィンドウ領域の
#          : 右下隅の座標。
pad.refresh( 0,0, 5,5, 20,75)

refresh() の呼び出しは、画面上の座標 (5,5) から (20,75) までの長方形に、パッドの一部を表示します。 表示する部分の左上隅は、パッド上の座標 (0,0) です。 この点以外は、パッドは通常のウィンドウとまったく同じで、同じメソッドを使えます。

画面上に複数のウィンドウやパッドがある場合は、各部分の更新に伴うちらつきを防ぎ、画面をより効率的に更新する方法があります。 refresh() は実際には次の 2 つの処理を行っています。

  1. それぞれのウィンドウの noutrefresh() メソッドを呼び出して、配下にある、スクリーンの望ましい状態を表すデータ構造を更新します。

  2. doupdate() 関数を呼び出して、データ構造に記録された望ましい状態に合致するように、物理スクリーンを更新します。

代わりに、複数のウィンドウで noutrefresh() を呼んでデータ構造を更新し、その後 doupdate() を呼んで画面を更新できます。

テキストの表示

C プログラマから見ると、curses は少しずつ異なる関数が入り組んだ迷路のように見えることがあります。 例えば、addstr() は stdscr ウィンドウの現在のカーソル位置に文字列を表示し、mvaddstr() は先に指定された y,x 座標へ移動してから表示します。 waddstr() は addstr() と同様ですが、デフォルトの stdscr の代わりに、使うウィンドウを指定できます。 mvwaddstr() ではウィンドウと座標の両方を指定できます。

幸運にも、Python インターフェースはこれらの詳細を全て隠蔽してくれます。 stdscr は他のものと同様のウィンドウオブジェクトであり、 addstr() のようなメソッドは複数の引数形式を許容してくれます。通常それらは4つの形式です。

形式

説明

str または ch

文字列 str または文字 ch を現在位置に表示します

str または ch, attr

文字列 str または文字 ch を属性 attr を利用して現在位置に表示します

y, x, str または ch

ウィンドウ内の位置 y,x に移動し str または ch を表示します

y, x, str または ch, attr

ウィンドウ内の位置 y,x に移動し属性 attr を利用して str または ch を表示します

属性によって表示するテキストをハイライトすることができます、ボールド体、アンダーライン、反転、カラーなど。より詳しくは次の小節で説明します。

addstr() メソッドは、表示する値として Python の文字列またはバイト列を受け取ります。 バイト列の内容はそのまま端末へ送られます。 ワイド文字に対応していないビルドでは、文字列はウィンドウの encoding 属性の値でエンコードされます。 この属性のデフォルト値は、locale.getencoding() が返すシステムのデフォルトエンコーディングです。

addch() メソッドは文字を受け取ります。 文字は、長さ 1 の文字列、長さ 1 のバイト列、または整数で指定できます。

端末の代替文字集合の文字に対応する定数が用意されています。 例えば、ACS_PLMINUS は +/- 記号、ACS_ULCORNER は枠の左上隅で、罫線を描くのに便利です。 対応する Unicode 文字を使うこともできます。

ウィンドウは最後の操作後のカーソル位置を覚えているので、y,x 座標を省略すると、文字列や文字は直前の操作でカーソルが残った位置に表示されます。 move(y,x) メソッドでカーソルを移動することもできます。 常に点滅するカーソルを表示する端末もあるので、邪魔にならない位置にカーソルを置くとよいでしょう。 無関係に見える場所でカーソルが点滅すると、混乱を招くことがあります。

アプリケーションで点滅するカーソルがまったく必要なければ、curs_set(False) を呼んで非表示にできます。 ウィンドウの leaveok() メソッドは、それとは異なる働きをします。 引数が真の場合、curses はカーソルをウィンドウのカーソル位置に戻さず、最後の更新で移動した位置に残します。

属性とカラー

文字にはさまざまな表示方法があります。 テキストベースのアプリケーションではステータス行を反転表示することが多く、テキストビューアでは特定の単語を強調表示したい場合があります。 curses では画面上の各セルに属性を指定して、これらを実現できます。

属性は整数値で、それぞれのビットが異なる属性を表わします。複数の属性ビットをセットしてテキストの表示を試みることができますが、curses は全ての組み合わせが利用可能であるかや視覚的に区別できるかどうかは保証してくれません、それらは利用している端末の能力に依存しているため、最も安全なのは、最も一般的に利用可能な属性を設定する方法です、ここに列挙します。

属性

説明

A_BLINK

テキストを点滅

A_BOLD

高輝度またはボールドテキスト

A_DIM

低輝度テキスト

A_REVERSE

反転テキスト

A_STANDOUT

利用できる最良のハイライトモード

A_UNDERLINE

下線付きテキスト

つまり、反転するステータスラインを画面の最上部に表示するには、コードをこうします:

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

curses ライブラリはカラー機能を提供している端末でのカラーもサポートしています。そんな端末の中で最も一般的なものは Linux コンソールで、color xterm もそれに続きます。

色を使うには、initscr() の直後に start_color() を呼び出し、デフォルトの色を初期化する必要があります (curses.wrapper() はこれを自動的に行います)。 初期化後、使用中の端末が実際にカラー表示に対応していれば、has_colors() は真を返します。 なお、curses はカナダやイギリスの綴り colour ではなく、アメリカの綴り color を使います。 イギリス式の綴りに慣れている方も、これらの関数では「綴り間違い」を我慢してください。

curses ライブラリは、前景色 (文字の色) と背景色からなる有限個のカラーペアを管理します。 color_pair() 関数で、カラーペアに対応する属性値を取得できます。 この値は A_REVERSE などのほかの属性とビット単位の OR で組み合わせられますが、その組み合わせがすべての端末で動くとは限りません。

例として、テキスト行をカラーペア 1 を使って表示します:

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

As I said before, a color pair consists of a foreground and background color. The init_pair(n, f, b) function changes the definition of color pair n, to foreground color f and background color b. Color pair 0 is hard-wired to white on black, and cannot be changed.

色には番号が付いており、start_color() はカラーモードを有効にするときに 8 つの基本色を初期化します。 0 は黒、1 は赤、2 は緑、3 は黄、4 は青、5 はマゼンタ、6 はシアン、7 は白です。 curses モジュールは各色に curses.COLOR_BLACK、curses.COLOR_RED などの定数を定義しています。

やってみましょう。カラー 1 を白背景に赤に変更してみましょう、こうして呼び出します:

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

カラーペアを変更すると、そのペアを使って既に表示されているテキストも新しい色になります。 次のように、その色で新たなテキストを表示することもできます。

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

Very fancy terminals can change the definitions of the actual colors to a given RGB value. This lets you change color 1, which is usually red, to purple or blue or any other color you like. Unfortunately, the Linux console doesn't support this, so I'm unable to try it out, and can't provide any examples. You can check if your terminal can do this by calling can_change_color(), which returns True if the capability is there. If you're lucky enough to have such a talented terminal, consult your system's man pages for more information.

ユーザ入力

C の curses ライブラリが提供する入力機能は、ごく単純なものだけです。 Python の curses モジュールは、基本的なテキスト入力ウィジェットを追加しています。 (Urwid などのほかのライブラリには、より豊富なウィジェットがあります。)

ウィンドウから入力を取得するメソッドは 3 つあります。

  • get_wch() は画面を更新してから、利用者のキー入力を待ちます。 事前に echo() が呼ばれていれば、入力されたキーを表示します。 待機する前にカーソルを移動する座標を指定することもできます。

  • getch() は同じ処理をしますが、文字の代わりにキーのコードを返します。 ncurses では、これは現在のロケールでエンコードされたキーの 1 バイトなので、複数バイトの文字を読むには、1 回に 1 バイトずつ複数回呼び出す必要があります。

  • getkey() は getch() と同じ処理をしますが、文字列を返します。 通常のキーは 1 文字の文字列、特殊キーは KEY_UP のような名前になります。

ウィンドウの nodelay() メソッドを使うと、利用者の入力を待たずに済むようにできます。 nodelay(True) の後は、そのウィンドウの読み取りはノンブロッキングになります。 入力がない場合、get_wch() と getkey() は例外を送出し、getch() は -1 を返します。 また、halfdelay() を使うと、実質的に各読み取りにタイマーを設定できます。 指定した待ち時間 (0.1 秒単位) 内に入力がなければ、読み取りは同じように失敗します。

Special keys such as Page Up, Home, or the cursor keys are returned by all three as one of the KEY_* constants, all larger than 255. You can compare the value returned to constants such as curses.KEY_PPAGE, curses.KEY_HOME, or curses.KEY_LEFT. The main loop of your program may look something like this:

while True:
    c = stdscr.get_wch()
    if c == 'p':
        PrintDocument()
    elif c == 'q':
        break  # while ループを抜ける
    elif c == curses.KEY_HOME:
        x = y = 0

curses.ascii モジュールには、整数または 1 文字の文字列を受け取り、ASCII の文字分類を判定する関数があります。 こうしたループの条件判定を読みやすく書くのに役立つでしょう。 また、整数または 1 文字の文字列を受け取り、同じ型を返す変換関数もあります。 例えば、curses.ascii.ctrl() は引数に対応する制御文字を返します。

1 行全体を取得する getstr() メソッドもあります。 機能がかなり限られるため、あまり使われません。 編集に使えるのは文字消去文字、行消去文字と、行を確定する Enter キーだけです。 バイト列オブジェクトを返し、取得するバイト数に上限を設けることもできます。

curses.echo()            # 入力文字の表示を有効にする

# カーソルを最上行に置き、最大 15 バイトの行を取得する
s = stdscr.getstr(0,0, 15)

curses.textpad モジュールは、Emacs に似たキーバインドを使えるテキストボックスを提供します。 Textbox の各種メソッドでは、入力を検証しながら編集したり、末尾の空白を残すか取り除くかを選んで編集結果を取得したりできます。 次に例を示します。

import curses
from curses.textpad import Textbox, rectangle

def main(stdscr):
    stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")

    editwin = curses.newwin(5,30, 2,1)
    rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
    stdscr.refresh()

    box = Textbox(editwin)

    # Ctrl-G が押されるまで、利用者に編集させる。
    box.edit()

    # 編集結果を取得する
    message = box.gather()

さらなる詳細についてはライブラリのドキュメント curses.textpad を参照してください。

より多くの情報

この HOWTO ではいくつかの進んだ話題、スクリーンスクレイピングや xterm インスタンスからマウスイベントを捉えるなど、については扱っていません。しかし、Python の curses モジュールのライブラリページはいまやかなり充実しています。次はこれを見るべきです。

curses 関数の詳しい動作に疑問があれば、使用している実装が ncurses であっても、プロプライエタリな Unix ベンダーのものであっても、その実装のマニュアルページを参照してください。 特有の動作に加え、利用可能なすべての関数、属性、ACS_* 文字の一覧が記載されています。

curses の API は非常に大きいため、Python のインターフェースではサポートしていない関数もあります。 多くの場合、実装が難しいからではなく、まだ誰も必要としていないためです。 また、Python はまだ ncurses に付属するメニューライブラリにも対応していません。 これらへの対応を追加するパッチを歓迎します。 Python へのパッチの提出方法については、Python Developer's Guide を参照してください。