[Python] PythonにおけるCursesプログラミング

はじめに

この記事は、Curses Programming with Pythonの日本語訳です。

PythonにおけるCursesプログラミング

A.M. Kuchling (amk@amk.ca), Eric S. Raymond (esr@thyrsus.com)

概要

この記事は、Python 2.xでテキストモードのプログラムを記述する方法について述べています。ディスプレイの制御には、curses拡張モジュールを使います。

この記事は、http://www.python.org/doc/howtoPython HOWTOで読めます。

目次

  • 1 cursesとは何か?
  • 1.1 Pythoncursesモジュール
  • 2 cursesアプリケーションの起動と終了
  • 3 ウィンドウとパッド
  • 4 テキストの表示
  • 4.1 属性と色
  • 5 ユーザからの入力
  • 6 より詳しい情報
  • この記事について...

cursesとは何か?

cursesライブラリは、端末に依存しない、テキスト端末の画面描画とキーボード処理の機能を提供します; これらの端末には、VT100sとLinuxコンソール、xtermやrxvtのようなX11プログラムの仮想端末が含まれます。端末は、カーソルを移動したり、画面をスクロールしたり、消去したりするといった、共通の動作を実行する様々な制御コードに対応します。端末が違えばコードも大きく異なり、しばしば独自の癖を持ちます。

Xの世界では、「なんで困ってるの?」と疑問に思うでしょう。文字端末が時代遅れな技術だというのは真実ですが、意匠を凝らしたことができることがいまだに価値があるニッチな領域が存在します。ひとつは、小さいfootprintまたは組み込みUnixで、これらはXサーバを持ちません。もうひとつは、OSインストーラやカーネル設定のようなツールで、これらはXが有効になる前に動作します。

cursesライブラリは、異なる端末間の細かい点を隠し、抽象的なディスプレイをプログラマに与え、それらは重なりあわない複数のウィンドウを持ちます。ウィンドウの表示内容は、テキストを追加し、消去し、外見を変更する様々な方法によって変えることができます。またcursesライブラリは、正しい出力を得るために端末に送信する必要がある制御コードは何かを自動的に理解します。

cursesライブラリは元々はBSD Unix向けに書かれました; その後AT&Tから出たUnixのSystem Vがたくさんの拡張と新しい関数を追加しました。BSD cursesはもうメンテナンスされておらず、AT&Tインターフェースのオープンソースの実装であるncursesに置き換わりました。もしあなたがLinuxFreeBSDといったオープンソースUnixを使っているならば、あなたのシステムはおそらくncursesを使っているでしょう。しかし、いくつかのプロプライエタリUnixが持っている古いバージョンのcursesは、すべてに対応していないでしょう。

誰もcursesモジュールをWindowsに移植していません。Windowsでは、Fredrik Lundhが書いたConsoleモジュールを使ってみて下さい。Consoleモジュールは、カーソル位置を指定してテキストを表示でき、さらにマウスとキーボードへ完全に対応しており、http://effbot.org/efflib/consoleから取得できます。

Python cursesモジュール

Pythonモジュールは、cursesが提供するC言語の関数をラップする単純なラッパーです; もしあなたがすでにC言語でのcursesプログラミングに詳しいなら、その知識をPythonに持ってくるのは本当に簡単です。もっとも大きな違いは、Pythonのインターフェースは、addstrやmvaddstr, mvwaddstrといったC言語の関数を、ひとつのaddstr()メソッドにまとめたことで、物事をより簡潔にしていることです。これについては、後で詳しく説明します。

このHOWTOは、cursesPythonを使ってテキストモードのプログラムを書くための、簡単紹介です。curses APIについて、完全な解説をしようとは思っていません; 完全な解説は、ncursesのPythonライブラリガイドと、ncursesのC言語のマニュアルを参照してください。しかしこの文章で、基本的な情報は得られます。

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

何かをする前に、cursesは初期化されなければなりません。これはinitscr()関数を呼ぶことでできます。これは端末のタイプを決定し、起動に必要なコードを端末に送信し、様々な内部データ構造を作成します。成功したら、initscr()は画面全体を表すウィンドウオブジェクトを返します; これは、対応するC言語の変数名から、普通stdscrと呼ばれます。

import curses
stdscr = curses.initscr()

通常cursesアプリケーションは、キー入力が自動的に画面に表示されないようにします。これは、キーを読み取り、特定の状況下でのみそれらを表示するためです。これには、noecho()関数を呼び出すことが必要です。

curses.noecho()

アプリケーションはまた普通、Enterキーを押すことなく、キー入力に直ちに反応することが必要です; これは、通常の入力がバッファされているモードではないという意味で、cbreadモードと呼ばれます。

curses.cbreak()

端末は普通、カーソルキーや、Page Up, Homeといった操作キーなどを、マルチバイトのエスケープシーケンスとして返します。あなたがそれらのシーケンスを正しく処理できるアプリケーションを書けるように、cursescurses.KEY_LEFTといった特殊な値を返します。cursesにこれをさせるため、あなたはキーパッドモードを有効にする必要があります。

stdscr.keypad(1)

cursesアプリケーションを終了するのは、開始するよりも簡単です。あなたは、cursesの端末の設定を元に戻すため、

curses.nocbreak(); stdscr.keypad(0); curses.echo()

を呼び出す必要があります。そして、endwin()関数を呼び出すと、端末を元のオペレーティングモードに復旧します。

curses.endwin()

cursesアプリケーションをデバッグするときの共通の問題点は、アプリケーションが端末を以前の状態に復旧することなしに終了したとき、あなたの端末が滅茶苦茶になることです。Pythonでは、これはあなたのコードにバグが含まれ、例外を捕捉しなかったときによく発生します。キーを押してももう画面には表示されず、例えば、シェルを使うのが難しくなります。

Pythonでは、curses.wrapperモジュールによって、あなたはこのようなやっかいな状況を回避でき、デバッグが非常に簡単になります。このモジュールにはwrapper関数が含まれ、これはhook引数を取ります。これは上で述べたような初期化を行い、もし色を表示することができたら、色を初期化します。それからhookを実行し、最後に適切な後始末を行います。hookはtry-catch句の中で呼び出され、そこで例外は捕捉され、cursesの後始末を行い、それから例外を上の層へ投げます。これにより、例外が発生したとき、あなたの端末がおかしな状態になったままであることがなくなります。

ウィンドウとパッド

ウィンドウは、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のやり方で、これを変更するのには遅すぎます。

あなたが文字列を表示したり消去したりするためにメソッドを呼び出したとき、その効果は画面に直ちに現れるわけではありません。これは、cursesがもともと遅い300ボーの端末で書かれたためです; これらの端末では、画面を再描画するのに必要な時間を最小化するのが、大変重要です。これにより、cursesは画面への変更を蓄積し、これらをもっとも効果のある方法で表示します。例えば、あなたのプログラムがウィンドウにいくつかの文字を表示し、それからウィンドウを消去したら、最初に書いた文字はもう見えないのだから、それらを送信する必要はありません。

このため、あなたはウィンドウオブジェクトのrefresh()メソッドを使って、ウィンドウを再描画することをcursesに明示的に伝える必要があります。実際、これはcursesのプログラミングをそれほど複雑にはしません。ほとんどのプログラムはごちゃごちゃと何かをしたあと、キーが押されるか、ユーザのなんらかのアクションを待つため、一端停止します。あなたは、ユーザの入力を待つために停止する前に、stdscr.refresh()を呼び出すか、適切なウィンドウのrefresh()メソッドを単純に呼び出すことで、画面が再描画されることを確認すればよろしい。

パッドは、ウィンドウの特殊なケースです; パッドは実際の画面よりも大きくでき、一部しか画面には表示されません。パッドを作成するのには単純にパッドの高さと幅が必要で、一方、パッドを再描画するためには、パッドの一部分が表示される画面上の領域の座標を与える必要があります。

pad = curses.newpad(100, 100)
#  These loops fill the pad with letters; this is
# explained in the next section
for y in range(0, 100):
    for x in range(0, 100):
        try: pad.addch(y,x, ord('a') + (x*x+y*y) % 26 )
        except curses.error: pass

#  Displays a section of the pad in the middle of the screen
pad.refresh( 0,0, 5,5, 20,75)

refresh()を呼び出すと、パッドの一部分が、座標(5, 5)から座標(20, 75)に広がる画面上の領域に表示されます; 表示される部分の左上の点は、パッド上で座標(0, 0)です。この違いを除いては、パッドは普通のウィンドウとまったく同じで、同じメソッドを持ちます。

もしあなたが複数のウィンドウとパッドを画面上に持っていたら、もっと効果的な方法があります。それは、再描画するときに、画面がちらつくのを抑えることができます。画面の望む状態を表現するデータ構造を更新するために、各ウィンドウのメソッドnoutrefresh()と/またはnoutrefresh()を使います; それから、関数doupdate()を使って、望む状態にあう物理的な画面を変更します。普通のrefresh()メソッドは、最後にdoupdate()を呼び出します。

テキストの表示

C言語プログラマの視点から見ると、cursesは紛らわしい関数を持っており、どれも少しずつ異なっています。例えば、addstr()はstdscrウィンドウの現在のカーソルの位置に文字列を表示しますが、mvaddstr()は与えられたy, x座標に最初に移動してから、文字列を表示します。waddstr()はaddstr()によく似ていますが、デフォルトでstdscrを使う代わりに、表示するウィンドウを指定できます。mvwaddstr()も同様です。

幸運なことに、Pythonのインターフェースはこれらの細かい点を隠します; stdscrは他と同じウィンドウオブジェクトで、addstr()のようなメソッドは複数の引数を取ります。引数には、以下の4つの形式があります。

形式 説明
str or ch 文字列strまたは文字chを表示します。
str or ch, attr 属性attrを使って、文字列strまたはchを表示します。
y, x, str or ch ウィンドウ内の位置y, xに移動して、strまたはchを表示します。
y, x, str or ch, attr ウィンドウ内の位置y, xに移動して、属性attrを使って、strまたはchを表示します。

属性によって、文字列を太字や下線、反転、色などで強調して表示できます。これらについては、この次の節で説明します。

addstr()関数は表示するPython文字列を取りますが、addch()関数は文字を取り、これは長さが1のPython文字列か整数です。もし文字列だった場合、0から255までの文字を表示することしかできません。SVr4のcursesは、拡張文字を表す定数を提供します; これらの定数は、255より大きいです。例えば、ACS_PLMINUSは+/-記号で、ACS_ULCORNERはボックスの左上の角です(境界線を引くのに便利です)。

ウィンドウは、最後の操作の後、カーソルがどこにあるかを覚えているので、あなたがy, x座標を忘れても、文字列または文字は、最後の操作が終わったところに表示されます。あなたは、move(y, x)メソッドでカーソルを移動させることもできます。いくつかの端末は常にカーソルを点滅させるため、あなたは気にならないどこかにカーソルを置こうとするかもしれません; 明らかにランダムな場所でカーソルが点滅するのは、混乱の元です。

もしあなたのアプリケーションが点滅するカーソルをまったく必要としていないなら、curs_set(0)を呼び出し、カーソルを非表示にできます。同じように、またcursesの古いバージョンとの互換性から、leaveok(bool)関数があります。boolがtrueなら、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コンソールで、xtermsの色にしたがっています。

色を使うためには、デフォルトの色の組み合わせを初期化するため、initscr()を呼び出した後でstart_color()関数を呼び出す必要があります(curses.wrapper.wrapper()関数はこれを自動的に行います)。一旦これが行われたら、使用している端末が実際に色を表示できる場合、has_color()関数はTRUEを返します(AMKからの注意: cursesはカナダ/イギリス流の'colour'ではなく、アメリカ流の'color'を使用します。もしあなたが私と同じなら、これらの関数をスペルミスしないように、自分自身を調整する必要があります)。

cursesライブラリは限られた色の組を管理しています。これらの組は、前面(文字)の色と、背景の色から構成されます。あなたは、ある色の組に対応した属性の値を得るため、color_pair()関数を使用します; この属性の値は、A_REVERSEのように、他の属性とORされたものですが、前述の通り、このような組み合わせがすべての端末で動作するかは保証されていません。

例えば、色の組1を使用して、文字列を1行表示するときは:

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

前述のように、色の組は前景色と背景色から構成されます。start_color()関数は色のモードを有効にするとき、8つの色を初期化します。それは、0: 黒、1: 赤、2: 緑、3: 黄、4: 青、5: マゼンタ、6: シアン、7: 白です。cursesモジュールはこれらの各色について定数を定義しています: curses.COLOR_BLACK, curses.COLOR_REDなどです。

init_pair(n, f, b)関数は色の組nの定義を、前景色fと背景色bに変更します。色の組0は黒地に白で、これは変更できません。

これらすべてを同時に使ってみましょう。色1を、白の背景色に赤い文字列に変更するときは、以下のようにします:

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

色の組を変更した場合、その色の組を使用して表示されている既存の文字列も新しい色に変更されます。あなたは以下のコードで、この色を使った新しい文字列を表示できます:

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

非常に意匠の凝った端末はRGBの値を指定することで実際の色の定義を変更できます。これにより、通常は赤である色1を、紫や青、あるいはその他の好きな色に変更できます。不幸なことに、Linuxのコンソールはこれに対応しておらず、そのため試すことも例をあげることもできません。あなたはcan_change_color()を呼び出すことで端末がこれに対応しているか調べることができ、もし可能であればこの関数はTRUEを返します。もし幸運にもそのようなtelnet端末を持っていたら、詳細はシステムのmanページを参照して下さい。

ユーザからの入力

cursesライブラリ自身は、非常に簡単な入力機構しか提供しません。Pythonでは、この欠点を補うための文字列を入力するウィジェットを追加しています。

ウィンドウへの入力を得るもっともよく使われる方法は、ウィンドウのgetch()メソッドを使うことです。これは一時停止し、ユーザのキー入力を待ち、もしecho()が前に呼び出されていたらそれを表示します。あなたは、一時停止する前にカーソルを移動させる座標を追加で指定することもできます。

nodelay()メソッドで、この挙動を変更することができます。nodelay(1)のあと、ウィンドウに対してgetch()すると、ノンブロッキングになり、入力がないときはERR (-1) を返します。halfdelay()関数というのもあり、(結果的に)それぞれのgetch()に時間制限を設定することができます; もしhalfdelay()の引数で指定したミリ秒だけ経過しても入力が得られない場合は、cursesは例外を生成します。

getch()メソッドは整数を返します; もし戻り値が0から255までの間ならば、それは押されたきーのASCIIコードを表します。255より大きい値は、Page UpやHome, カーソルキーといった特殊キーを表します。あなたは戻り値を、curses.KEY_PPAGEやcurses.KEY_HOME, curses.KEY_LEFTといった定数と比較することができます。通常、あなたのメインループは以下のようになります:

while 1:
    c = stdscr.getch()
    if c == ord('p'): PrintDocument()
    elif c == ord('q'): break  # Exit the while()
    elif c == curses.KEY_HOME: x = y = 0

curses.asciiモジュールは、ASCIIクラスのクラス関数を提供し、この関数は整数か1文字の文字列を引数に取ります; これはあなたのコマンドインタプリタの読みやすいテストを書くのに便利です。このクラスはまた、整数か1文字の文字列を引数に取り、相互に変換する関数も提供します。例えば、curses.ascii.ctrl()は引数に対応した制御文字を返します。

完全な文字列を受け取るgetstr()というメソッドもあります。これはあまり使わないでしょう、なぜならば機能が非常に限られているからです; 編集キーで使用できるのはバックスペースキーと文字列を終了させるEnterキーだけです。これは文字列の長さを制限することもできます。

curses.echo()            # Enable echoing of characters

# Get a 15-character string, with the cursor on the top line 
s = stdscr.getstr(0,0, 15)

Pythoncurses.textpadモジュールはもう少しましなものを提供します。これを使えば、テキストボックスを表示でき、そこでEmacsのようなキーバインディングで操作できます。Textboxクラスの様々なメソッドは入力値検証を伴う編集に対応し、前後の空白を取り除いたり除かなかったりして結果を返します。詳細は、curses.textpadのライブラリのドキュメントを読んで下さい。

より詳しい情報

このHOWTOは上級者向けの事柄、例えば画面を切り取ったりxtermのインスタンスからマウスのイベントを捕捉するといったことまで説明していません。しかしPythonのライブラリのcursesモジュールのページは、現在は完璧です。あなたは次にこれを読んだらいいです。

ncursesの詳細な挙動について不審に思ったら、あなたのcurses実装のマニュアルページを、その実装がncursesであってもプロプライエタリUnixベンダーのものであっても、参照して下さい。マニュアルページには癖や関数、属性、使用可能なACS_*文字の完全なリストがあります。

curses APIは巨大なので、いくつかの機能はPythonのインターフェースではサポートしていません。しかしそれは実装するのが難しいからではなく、誰もいまだに必要としていないからです。それらを追加して、パッチを送付するのは自由です。また、ncursesにおけるメニューやパネルの対応もしていません; それを追加するのは自由です。

もしあなたが小さい面白いプログラムを書いたら、新しいデモとして供出するのは自由です。私たちはいつでもそれらを使えます!

ncurses FAQ: http://dickey.his.com/ncurses/ncurses.faq.html

この記事について...

PythonにおけるCursesプログラミング

この文章は、LaTeX2HTMLによって生成されました。

LaTeX2HTML is Copyright (c) 1993, 1994, 1995, 1996, 1997, Nikos Drakos, Computer Based Learning Unit, University of Leeds, and Copyright (c) 1997, 1998, Ross Moore, Mathematics Department, Macquarie University, Sydney.

LaTeX2HTMLPythonの文書に適用するのは、Fred L. Drake, Jr.が大いに調整しました。移動アイコンはChristopher Petrilliから提供されました。