cursesライブラリ

画面とキー入力制御の自由度を高める curses ライブラリを用いると、高機能な対話的プログラムが作れる。

cursesの利用でできること

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

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

これらのことに気をつけ、それぞれのメソッドをcurses専用の ものを必ず利用するように気をつけていれば問題ない。

cursesの導入

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

#!/usr/koeki/bin/ruby
require 'curses'
include Curses

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

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

画面制御

エスケープシーケンス (基礎プログラミングI) を用いても画面の任意の位置に文字出力できるが、位置決めを頻繁に行なう ようなプログラムではcursesを用いる方がプログラムを作りやすい。

たとえば、画面上「5行目、10桁目」に「こんにちは」と表示する プログラムは以下のようになる。

cur-hello.rb

#!/usr/koeki/bin/ruby
require 'curses'
include Curses

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

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

即時キー入力

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

Cursesライブラリに含まれる getch メソッドは入力キー (の文字コード)を1字文だけ読み取るもので、これを呼ぶ前に cbreak メソッドを呼んでおくと、Return を押さなくてもデータが getch メソッドに伝わるようになる。 以下の2つの例を試してみよ。

Returnキーを押すまで進まない:

cur-nocbreak.rb

#!/usr/koeki/bin/ruby
require 'curses'
include Curses

init_screen                     # スクリーンを初期化する
nocbreak                        # Returnでデータをまとめて送る


begin
  addstr("何かキーを押してください: ")
  x = getch                     # 1字読み取る(文字コードが返る)
  addstr("\nさようなら(3秒後に終わります)")
  refresh                       # 出力を画面に反映させる
  sleep 3                       # 結果がしばらく見えるようにする
ensure
  close_screen
end

Returnキーを押さなくても進む:

cur-cbreak.rb

#!/usr/koeki/bin/ruby
require 'curses'
include Curses

init_screen                     # スクリーンを初期化する
cbreak                          # リターンキーなしでも入力させる
noecho                          # 入力文字のエコーバックを無しにする

begin
  addstr("何かキーを押してください(打った文字は見えません): ")
  x = getch                     # 1字読み取る(文字コードが返る)
  addstr("\nさようなら(3秒後に終わります)")
  refresh                       # 出力を画面に反映させる
  sleep 3                       # 結果がしばらく見えるようにする
ensure
  close_screen
end

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

制限時間つきキー入力

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

cur-timeout.rb

#!/usr/koeki/bin/ruby
require 'curses'
include Curses
cbreak
Curses.timeout = 2500           # 2.5秒でアウト
init_screen

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

begin
  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
require 'curses'
include Curses

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) で利用する

begin
  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")
  while true
    addstr("何番の色にしますか?: ")
    choice = getch.chr.to_i
    break if choice > 0 && choice <= nc
    attron(color_pair(warn)) do addstr("\n1〜#{nc}の範囲にしてね。\n") end
  end
  bkgd(color_pair(choice))      # 標準の前景・背景色を設定
  addstr("\n")
  refresh
  sleep 2
ensure
  close_screen
end

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

m & n ビットAND
m | n ビットOR
m ^ n ビットXOR
~nビットXOR
m << n mnビット右シフト
m >> n mnビット左シフト

サブウィンドウ

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

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

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

cur-subwin.rb

#!/usr/koeki/bin/ruby -Ke
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.bkgd(color_pair(tcolor)) # 内部子ウィンドウの標準色変更
  # 以後は内部の子ウィンドウに対する処理のみ
  subw.setpos(0, 0)             # 子ウィンドウの左上隅に移動しておく
  i = 0
  while true                    # 枠内に「n行目\n」を永遠に出し続ける
    subw.addstr(sprintf("%2d行目\n", i+=1))
    subw.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)
  end
  t2 = Thread.new do            	  # 右窓を操作するスレッド生成
    winbox(rightwin, false, 2)
  end
  setpos(0, 0)                  # 画面先頭にメッセージ出力
  addstr("何かキーを押すと止まります")
  refresh
  getch                         # 1字読み捨てて終了
ensure
  close_screen
end

キーパッド

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

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

先頭の ^[ はこの記号2つでESC文字1字を表す表記で、 実際にカーソルを移動してみると ^[ の部分が分割できない 1字になっていることが分かる。ktermで特殊キーに標準的に割り当てられている 文字列は以下のとおりである(^[は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 を見ると分かる。ただし、 すべてのキーが読み取り可能とは限らない。keypad(true) とする場合でも、特殊キーが入力しやすい場所にあるとは限らないので ASCIIコードを持つ普通のキーにも同じ機能を持たせるように設計すべきである。 たとえば、上下左右を↑↓←→キーで操作する機能を付けるならば、 同等のキー割り当てを C-pC-nC-bC-f や、 kjhl にも 割り当てる。前者はEmacsの、後者はviの標準キー割り当てなので 説明なしでもとっさに使いこなしてもらえる可能性が高い。

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

その他必要なメソッド

Cursesライブラリを用いた対話的なプログラムをしっかり作るためには 以下のメソッドや変数を覚えておくよい。

サンプルプログラム

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

cur-jump.rb

#!/usr/koeki/bin/ruby
# 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

目次