cursesライブラリ

これまで作成したプログラムを利用する場合,利用者がプログラムに対して 行なうアクションは「文字列を入力してEnterを押す」 という一方向のものばかりであった。 また,画面への出力も上から下へと流れるものしかなかったが, curses ライブラリを用いると, より高度で自由度の高い対話的プログラムが作れる。

なお,Ruby バージョン 2.1 からは,curses が標準ライブラリから外れたため, 利用するためには追加インストールする必要がある。 まず,利用中のRubyでcursesが使えるか確認するため,以下のように起動する。

ruby -rcurses -e ''

もし,これでエラーメッセージが出るようであればシステムのRubyに curses ライブラリが組み込まれていない。その場合はスーパーユーザ等十分な権限で インストールを行なう。一例を示す。

sudo gem install curses

cursesの利用でできること

cursesをプログラム内に導入し,利用宣言すると以下のことが可能になる。

その一方,画面出力・キー入力がすべてcursesのものになるため, 普通に標準入出力を用いているプログラムで可能な以下のことが できなくなる

ただし,いずれも curses 専用の制御メソッドを利用するよう気を付ければ問題ない。

cursesの導入

cursesを利用したプログラムは以下のような流れで作成する。

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

init_screen                     # スクリーンを初期化する

begin
  〜 必要な処理本体 〜
ensure
  close_screen                  # スクリーンを元に戻す
end

cursesの機能を提供するメソッドは Curses::メソッド名 によって呼び出す。ただし上のように記述すれば4行目の include 文によって Curses:: の接頭辞なしでメソッド呼び出しが可能になる。

cursesでは,端末画面の様々な設定値を変更するため, 最後に設定値を元に戻す処理を行なわないと, その後の端末操作に異常を来たす場合がある。 このためプログラムが異常終了した場合でも確実に close_screen が行なわれるよう ensure ブロックで確実に処理する。

画面制御

エスケープシーケンス (基礎プログラミングI) を用いても画面の任意の位置に文字出力できるが, 決められた規則にしたがって記号を出力する必要があるため 位置決めを頻繁に行なうのは非常に煩雑である。 cursesを用いると,仮想端末の画面を自在にレイアウトするプログラムを 容易に作ることができる。 たとえば,画面上「5行目,10桁目」に「こんにちは」と表示する プログラムは以下のようになる。

cur-hello.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

init_screen                     # スクリーンを初期化する

begin
  setpos(4, 9)                  # 5行目の10桁目(0から数えるため)
  addstr("こんにちは")
  refresh                       # 出力を画面に反映させる
  sleep 3                       # これがないとすぐ消えてしまうため
ensure
  close_screen
end

プログラム中で利用している setpos, addstr, refresh メソッドは以下のような働きを持つ。

即時キー入力

これまで作成してきたようなデータの入力を主目的とするプログラムでは, 意味のある文字列を打ち込んで最後にReturnキーを押して 初めてデータがプログラム(getsメソッド)に渡る。 データ入力では間違えずに入れることが大切なのでこれでよいが,たとえば 「準備ができたら何かキーを押してください」, 「yかnを押してください」のような場合はReturn を押さずに進めた方が利用者にしてみれば快適である。

cursesライブラリに含まれる getch メソッドは入力キーを1字分だけ読み取るもので,これを呼ぶ前に cbreak メソッドを呼んでおくと,Return を押さなくてもデータが getch メソッドに伝わるようになる。 逆に,nocbreak メソッドを呼んでおくと Return を押すまで先に進まない。 以下の2つの例を試してみよ。

後者で利用した noecho は,入力した文字を 画面に出すこと(エコーバック)をやめる。

制限時間付きキー入力

Curses.timeout 変数にミリ秒単位の整数を指定すると getch メソッドで入力を待つ制限時間となる。たとえば, 入力待ちを2.5秒で打ち切るには Curses.timeout = 2500 とする。

cur-timeout.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses
cbreak
Curses.timeout = 2500           # 2.5秒でアウト

srand
alphabet = "abcdefghijklmnopqrstuvwxyz"
ans = alphabet[rand(alphabet.length)]

begin
  init_screen
  setpos(5,10)
  addstr(sprintf("%cを押せ! : ", ans))
  refresh
  key = getch
  setpos(6,10)
  if key == ans
    addstr("正解! また会おう")
  else
    addstr("出直してこい")
  end
  refresh
  sleep 2
ensure
  close_screen
end

文字属性変更

curses ライブラリのメソッドで行なう画面出力ではエスケープシーケンス などは使えない。代わりに,curses 独自の方法で前景色・背景色・文字飾り などの属性変更を行なう。

主な用途としては文字に色を付けることが考えられる。curses では curses 管轄下の「色番号」を使って属性を管理する。そのためには, まず,自分で決めた色番号に対して,前景色と背景色を組にしたものを 定義する。定義したのちに,属性をセットするメソッドを利用する。 ここでは使い勝手のよい attron を利用した例を示す。

cur-color.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

begin
  init_screen
  has_colors? or abort("この端末では色が使えません")
  cbreak
  start_color                     # 必ずinit_screenのあと、init_pairの前
  # init_pair(0, COLOR_BLACK, COLOR_WHITE) # 白のままだね
  init_pair(1, COLOR_RED, COLOR_BLACK)
  init_pair(2, COLOR_GREEN, COLOR_BLACK)
  init_pair(3, COLOR_YELLOW, COLOR_BLACK)
  init_pair(4, COLOR_BLUE, COLOR_BLACK)
  init_pair(5, COLOR_MAGENTA, COLOR_BLACK)
  init_pair(6, COLOR_CYAN, COLOR_BLACK)
  init_pair(7, COLOR_WHITE, COLOR_BLACK)
  init_pair(8, COLOR_BLACK, COLOR_WHITE)
  init_pair(9, COLOR_BLUE, COLOR_YELLOW)
  nc = 9
  init_pair(warn=10, COLOR_RED, COLOR_WHITE)
  # 以上定義した色番号nは color_pair(n) で利用する

  clear
  setpos(0, 0)
  1.upto(nc) do |i|
    addstr("#{i}: ")
    attron(color_pair(i)) do
      addstr("こんにちは ")
      attron(A_BOLD) do
        addstr("太こんにちは ")
      end
      attron(A_REVERSE) do
        addstr("逆こんにちは ")
      end
      attron(A_REVERSE|A_BOLD) do
        addstr("逆太こんにちは ")
      end
    end
    addstr("\n")
  end
  refresh
  addstr("何かキーを押すと終了します.\n")
  refresh
  getch
ensure
  close_screen
end

attron による属性設定は,色や属性を表す ビット値の合成値を指定する。Rubyによるビット演算子一覧を示しておく。

ビット演算子働き
m & n ビットAND
m | n ビットOR
m ^ n ビットXOR
~nビット反転
m << n mnビット左シフト
m >> n mnビット右シフト

サブウィンドウ

curses が統轄する画面は Curses::Window というクラスのオブジェクトで,最初のウィンドウは stdscr 変数に割り当てられたオブジェクトである。これまで説明したメソッドはすべて stdscr に対する処理を行なうためのものである。

curses では stdscr とは別の新たなウィンドウを生成して そのウィンドウだけを処理対象とすることができる。プルダウンメニューや 会話的出力の箱型ウィンドウなどはサブウィンドウを用いて作成する。

サブウィンドウは subwin メソッドで生成する。 Curses::Window オブジェクトを返すのでその値を 保存しておき,サブウィンドウ内への出力を行ないたい場合に それを利用する。

たとえば端末画面の10行目5桁の位置に, 10行×30桁のサブウィンドウを作り枠を作るには以下のようにする。

cur-box.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

begin
  init_screen
  w = stdscr.subwin(5, 36, 10, 5)	# サブウィンドウを作成し w に格納
  w.box("|"[0], "-"[0])                 # サブウィンドウに枠付け
  w.setpos(2, 10)			# サブウィンドウ内の位置指定
  w.addstr("キーを押すと終了")          # サブウィンドウに表示
  w.refresh
  w.getch
ensure
  close_screen
end

これにより以下のようなウィンドウが出現する。

Example of subwindow

サブウィンドウを作成した場合, 親ウィンドウとの境目を明確にするため,上の例のように box メソッドによって枠を付けると見やすくなる。 ただし,枠付きのサブウィンドウに大量の文字列を表示したい場合は 枠を描く文字が上書きされないよう, 内側にさらに小さいサブウィンドウを作成してそこに文字列を 表示させるとよい。

サブウィンドウを含めたウィンドウには,それぞれ独立した 挙動の属性を設定することができる。たとえば,ウィンドウ内部の出力文字列を スクロールさせるかは scrollok メソッドによって設定できる。 また,上の例示プログラムにあるように,refresh メソッド, getch メソッドもサブウィンドウ固有のものが使える。 さらに,サブウィンドウごとに getch のタイムアウト値も設定できる。

これらのことを利用し,2つの枠付ウィンドウを作成し,さらにそれぞれの 内部にスクロール可能なサブウィンドウ,スクロールしないサブウィンドウを 作成して表示実験を行なうプログラムを示す。

cur-subwin.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

def winbox(win, scroll, tcolor) # 枠を付け文を出し続けるメソッド
  win.box("|"[0], "-"[0])       # まず枠を付ける
  # 次に子ウィンドウを枠の内部に作成
  subw = stdscr.subwin(win.maxy-2, win.maxx-2, win.begy+1, win.begx+1)
  subw.scrollok(scroll)         # スクロールの許可を決める
  win.setpos(0, 2)              # 枠を上書きする位置にタイトル表示
  win.addstr(sprintf("[スクロール %s]", scroll.inspect))
  win.refresh                   # winに対する変更を反映
  # 以後は内部の子ウィンドウに対する処理のみ
  subw.setpos(0, 0)             # 子ウィンドウの左上隅に移動しておく
  i = 0
  while true                    # 枠内に「n行目\n」を永遠に出し続ける
    subw.attron(color_pair(tcolor)) do
    	subw.addstr(sprintf("%2d行目\n", i+=1))
    end
    subw.refresh
    # refresh			# このrefreshの有無でカーソル位置が違う
    sleep 0.1
  end
end

begin
  init_screen
  has_colors? or abort("この端末では色が使えません。")
  cols>=72 && lines>=20 or abort("端末の大きさ72x20以上で起動してください。")

  start_color
  init_pair(1, COLOR_RED, COLOR_WHITE)    # 1=白地に赤
  init_pair(2, COLOR_YELLOW, COLOR_BLACK) # 2=黒地に黄

  leftwin = stdscr.subwin(10, 30, 10, 05) # 左側子ウィンドウ作成
  rightwin = stdscr.subwin(10, 30, 5, 40) # 右側子ウィンドウ作成
  t1 = Thread.new do                      # 左窓を操作するスレッド生成
    winbox(leftwin, true, 1)		  # 第2引数がスクロール指定
  end
  t2 = Thread.new do            	  # 右窓を操作するスレッド生成
    winbox(rightwin, false, 2)		  # 第2引数がスクロール指定
  end
  setpos(0, 0)                  # 画面先頭にメッセージ出力
  attron(color_pair(2)) {
  addstr("何かキーを押すと止まります")
  }
  refresh
  getch                         # 1字読み捨てて終了
  # t1.kill; t2.kill
ensure
  close_screen
end

この例では左右それぞれの枠付ウィンドウ内部で文字列を 連続して表示させるため,2つのスレッドを生成して同時実行させている。 実行してしばらく経過した様子を以下に示す。

cur-subwin.rb screen shot

この例の左側のウィンドウは自動スクロールが有効化されているため 次々と行が出力されているが, 右側では無効化されているため8行目以降はどんどん外れて行っている。

キーパッド

キーボードで,英数字・記号の割り当てられているキーをタイプすると 端末上ではその1字に対応するASCIIコードがプログラムに送られる。 しかし,矢印キーやファンクションキーなどの特殊キーはそれに対応する ASCII文字があるわけではなく, 標準ではESC文字から始まるエスケープシーケンスを まとめて入力したのと同じことになる。端末上で C-v をタイプしてから上矢印キーを押すと, どのような文字列が生成されるかが分かる。

C-v
(^[[A と出てくる)

先頭の ^[ はこの記号2つでESC文字1字を表す表記で, 実際にカーソルを移動してみると ^[ の部分が分割できない 1字になっていることが分かる。端末上で特殊キーに標準的に割り当てられている 文字列は以下のとおりである(^[はESC文字)。

キー割り当てられている文字列
^[[A
^[[B
^[[C
^[[D
F1^[[11~
F2^[[12~
: :
F12^[[24~

プログラム中で利用者に矢印キーやファンクションキーを使わせたい場合, それらが複数文字を送り込んで来ることを想定するのはたいへんなので, cursesでは特殊キーをまとめて1個のシンボルとして getch できるモードが用意されている。

特殊キー読み込みを利用する場合は,利用したいウィンドウオブジェクト に属する keypad メソッドを呼ぶ。たとえば,メインウィンドウ (stdscr)で読み取る getch で特殊キーを使いたい 場合は

stdscr.keypad(true)

とする。この場合 getch で特殊キーを押したときに返る値は 以下のようなシンボルで定義されている値となる。

キーgetchで返る値
KEY_UP
KEY_DOWN
KEY_RIGHT
KEY_LEFT

どのような特殊キーがどんな値を返すかは,おおむね /usr/include/curses.h ファイル内で define されている KEY_ で始まるシンボルを見ると分かる。ただし, すべてのキーが読み取り可能とは限らない。keypad(true) とする場合でも,特殊キーが入力しやすい場所にあるとは限らないので ASCIIコードを持つ普通のキーにも同じ機能を持たせるように設計すべきである。 たとえば,上下左右を↑↓←→キーで操作する機能を付けるならば, 同等のキー割り当てを C-pC-nC-bC-f や, kjhl にも 割り当てる。前者はEmacsの,後者はviの標準キー割り当てなので 説明なしでもとっさに使いこなしてもらえる可能性が高い。

矢印キーでマークを移動できる プログラムの例を cur-keypad.rb に示す。

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

begin
  init_screen
  x = cols/2
  y = lines-2
  noecho
  stdscr.keypad=true            # ここをコメントアウトしてみよ
  setpos(y, x)                  # 最初の1個を書いておく
  addstr("●")
  setpos(0, 0)
  addstr("←→かhlで左右,F9かcで中央に戻す。F10かqで終了")

  refresh
  while true
    c = getch
    setpos(y, x)
    addstr("  ")                # 以前書いた丸を消す
    case c
    when KEY_LEFT, ?h
      x -= 1
    when KEY_RIGHT, ?l
      x += 1
    when KEY_F9, ?c
      x = cols/2
    when ?q, KEY_F10, ?\n
      break
    else
      setpos(1, 0)
      addstr(sprintf("不明なキー: %s", c.inspect))
    end
    x = 1 if x < 1
    x = cols-2 if x > cols-2
    setpos(y, x)
    addstr("●")
    setpos(1, 0)                # 邪魔なのでカーソルをどかす
    refresh
  end
ensure
  close_screen
end

その他必要なメソッド

curses ライブラリを用いた対話的なプログラムを作るために有用な メソッドや変数のうち主要なものを示す。

cur-insdel.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

init_screen
x, y = cols/4, lines/2
cbreak
noecho
setpos(y-3, 0)
addstr("タイプした文字を逆向きに挿入します。\n")
addstr("(Returnで終了,C-h(BS)で後方削除)")
setpos(y, x)
refresh

begin
  while (ch=getch).ord != ?\n.ord
    # .ord は文字コードを得るメソッド。
    # Ruby1.8ではgetchは入力文字の文字コードを返す一方,
    # Ruby1.9ではgetchは長さ1の文字列を返すのだが,
    # 制御文字だけは文字コードで返すため,両方強制的に文字コードに
    # 変換してから比較するようにしないと1.8と1.9で動かない。
    setpos(y-1, x)
    addstr(sprintf("key = %s", ch.inspect))
    setpos(y, x)
    if ch.ord == 0x8.ord        # C-h
      delch
    else
      insch(ch)
    end
    refresh
  end
ensure
  close_screen
end

cur-line.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses

begin
  init_screen
  y = lines/3
  cbreak
  noecho
  setpos(y, 0)
  addstr("キーを押すと終了")
  refresh

  Thread.new do
    getch                       # キー入力があったら
    exit                        # exitするだけのスレッド
  end
  while true
    0.upto(9) do |i|
      setpos(y, 0)
      insertln                  # 1行挿入してから
      addstr("行番号 #{i}")     # 文字列出力
      sleep 0.2
      refresh
    end
    setpos(y, 0)
    0.upto(9) do |i|
      deleteln                  # y行目を1行削除
      refresh
      sleep 0.2
    end
  end
ensure
  close_screen
end

サンプルプログラム

cursesを用いたアクション型プログラムの例を示す。

cur-jump.rb

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
# cursesを用いて●を動かす
require 'curses'
include Curses
noecho                          # エコーバックなし
cbreak                          # Returnなしで即入力
Curses.timeout = 0		# 入力は待たない

init_screen			# 画面も消える
ball = "●"
kesu = "  "
wait = 0.03			# タイマー

x = 0
y = lines-2			# 下から2行目
j = 0                           # ジャンプの高さ
jmax = 6                        # 2ステップ分高度をあげる
jnow = 0                        # 現在のステップ(0〜3)

setpos(1, 0)
addstr("SPCでジャンプ!")
setpos(y-5, cols/2+rand(3))     # ランダムに決めた位置に
addstr("★")                    # ★を置く
begin
  h = y-1                       # 高さの初期値をセットしておく
  while x < cols			# 右から左へ
    setpos(h, x-1)		# カーソルを今の位置へ
    addstr(kesu)			# 前のボールを消す
    x += 1
    h = y-1 - ((jmax-jnow)*jnow/2) # ジャンプは2次曲線
    setpos(h, x-1)		# カーソルを次の位置へ
    addstr(ball)		# ボールを書く
    setpos(0,0)			# カーソルを邪魔でないところへ
    refresh                     # これをしないと画面に反映されない
    if jnow > 0 then
      jnow -= 1                 # ジャンプ中の処理
      getch                     # ジャンプ中に押されたキーは捨てる
    else
      key = getch
      if key == " "[0]          # SPCだったら
        jnow = jmax             # ジャンプ開始
      end
    end
    sleep(wait)			# 一定時間休む
  end
  setpos(y-1, 0)
  addstr("おしまい\n")
  refresh                       # 最後も忘れずに
  sleep 3
ensure
  close_screen
end

実際に動かすと,●記号が左から右に向かって進み, スペースキーを押すとジャンプする。 スペースキーを押した直後の画面の様子を以下に示す。

cur-jump.rb screen shot


目次