これまで作成したプログラムを利用する場合,利用者がプログラムに対して 行なうアクションは「文字列を入力してEnterを押す」 という一方向のものばかりであった。 また,画面への出力も上から下へと流れるものしかなかったが, curses ライブラリを用いると, より高度で自由度の高い対話的プログラムが作れる。
なお,Ruby バージョン 2.1 からは,curses が標準ライブラリから外れたため, 利用するためには追加インストールする必要がある。 まず,利用中のRubyでcursesが使えるか確認するため,以下のように起動する。
ruby -rcurses -e ''
もし,これでエラーメッセージが出るようであればシステムのRubyに curses ライブラリが組み込まれていない。その場合はスーパーユーザ等十分な権限で インストールを行なう。一例を示す。
sudo gem install curses
cursesをプログラム内に導入し,利用宣言すると以下のことが可能になる。
その一方,画面出力・キー入力がすべてcursesのものになるため, 普通に標準入出力を用いているプログラムで可能な以下のことが できなくなる。
puts, print, printf
     などを使った文字列出力(期待どおりの場所に出ない)
明示的に設定しなければ出力画面の自動スクロールがされない (最下行で止まる)
getsなどの入力関数(データを送れない)
ただし,いずれも 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桁目」に「こんにちは」と表示する プログラムは以下のようになる。
#!/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
メソッドは以下のような働きを持つ。
setpos(y, x)
     端末画面上 y+1 行目,x+1 桁目にカーソル位置を設定する。
addstr(string)
     端末画面上の現在のカーソル位置に文字列 string を表示する。
refresh
     それまでの出力を実際に端末画面に反映させる。
これまで作成してきたようなデータの入力を主目的とするプログラムでは,
意味のある文字列を打ち込んで最後にReturnキーを押して
初めてデータがプログラム(getsメソッド)に渡る。
データ入力では間違えずに入れることが大切なのでこれでよいが,たとえば
「準備ができたら何かキーを押してください」,
「yかnを押してください」のような場合はReturn
を押さずに進めた方が利用者にしてみれば快適である。
cursesライブラリに含まれる getch
メソッドは入力キーを1字分だけ読み取るもので,これを呼ぶ前に
cbreak メソッドを呼んでおくと,Return
を押さなくてもデータが getch メソッドに伝わるようになる。
逆に,nocbreak メソッドを呼んでおくと
Return を押すまで先に進まない。
以下の2つの例を試してみよ。
Returnキーを押すまで進まない:
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses
begin
  init_screen                   # スクリーンを初期化する
  nocbreak                      # Returnでデータをまとめて送る
  addstr("何かキーを押してください: ")
  x = getch                     # 1字読み取る
  addstr("\nさようなら(3秒後に終わります)")
  refresh                       # 出力を画面に反映させる
  sleep 3                       # 結果がしばらく見えるようにする
ensure
  close_screen
end
Returnキーを押さなくても進む:
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'curses'
include Curses
begin
  init_screen                   # スクリーンを初期化する
  cbreak                        # リターンキーなしでも入力させる
  noecho                        # 入力文字のエコーバックを無しにする
  addstr("何かキーを押してください(打った文字は見えません): ")
  x = getch                     # 1字読み取る
  addstr("\nさようなら(3秒後に終わります)")
  refresh                       # 出力を画面に反映させる
  sleep 3                       # 結果がしばらく見えるようにする
ensure
  close_screen
end
後者で利用した noecho は,入力した文字を
画面に出すこと(エコーバック)をやめる。
Curses.timeout 変数にミリ秒単位の整数を指定すると
getch メソッドで入力を待つ制限時間となる。たとえば,
入力待ちを2.5秒で打ち切るには Curses.timeout = 2500 とする。
#!/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 を利用した例を示す。
#!/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 | 
  mのnビット左シフト | 
m >> n | 
  mのnビット右シフト | 
curses が統轄する画面は Curses::Window
というクラスのオブジェクトで,最初のウィンドウは stdscr
変数に割り当てられたオブジェクトである。これまで説明したメソッドはすべて
stdscr に対する処理を行なうためのものである。
curses では stdscr とは別の新たなウィンドウを生成して
そのウィンドウだけを処理対象とすることができる。プルダウンメニューや
会話的出力の箱型ウィンドウなどはサブウィンドウを用いて作成する。
サブウィンドウは subwin メソッドで生成する。
Curses::Window オブジェクトを返すのでその値を
保存しておき,サブウィンドウ内への出力を行ないたい場合に
それを利用する。
win.subwin(lines, cols,
     y, x)
     ウィンドウ win の内部に lines行×cols 桁のサブウィンドウを作成する。 サブウィンドウの左上の位置は win 内の y+1 行・x+1 桁目とする。
win.box(vch, hch)
     ウィンドウ win の最外郭に枠を付ける。 枠左右の縦線に使う文字は vch, 枠上下の横線に使う文字は hch にする。
たとえば端末画面の10行目5桁の位置に, 10行×30桁のサブウィンドウを作り枠を作るには以下のようにする。
#!/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
これにより以下のようなウィンドウが出現する。

サブウィンドウを作成した場合,
親ウィンドウとの境目を明確にするため,上の例のように box
メソッドによって枠を付けると見やすくなる。
ただし,枠付きのサブウィンドウに大量の文字列を表示したい場合は
枠を描く文字が上書きされないよう,
内側にさらに小さいサブウィンドウを作成してそこに文字列を
表示させるとよい。
サブウィンドウを含めたウィンドウには,それぞれ独立した
挙動の属性を設定することができる。たとえば,ウィンドウ内部の出力文字列を
スクロールさせるかは scrollok メソッドによって設定できる。
また,上の例示プログラムにあるように,refresh メソッド,
getch メソッドもサブウィンドウ固有のものが使える。
さらに,サブウィンドウごとに getch
のタイムアウト値も設定できる。
これらのことを利用し,2つの枠付ウィンドウを作成し,さらにそれぞれの 内部にスクロール可能なサブウィンドウ,スクロールしないサブウィンドウを 作成して表示実験を行なうプログラムを示す。
#!/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つのスレッドを生成して同時実行させている。 実行してしばらく経過した様子を以下に示す。

この例の左側のウィンドウは自動スクロールが有効化されているため 次々と行が出力されているが, 右側では無効化されているため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-p,C-n,C-b,C-f や,
k,j,h,l にも
割り当てる。前者は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 ライブラリを用いた対話的なプログラムを作るために有用な メソッドや変数のうち主要なものを示す。
cbreak
     Return キーなしで即入力データを渡す。
nocbreak
     Return キー入力を待ってから入力データを渡す。
echo
     タイプした文字を画面にエコーバックする。
noecho
     タイプした文字を画面にエコーバックしない。
clear
     画面全体を表すウィンドウをクリアする。
closed?
     すでにcurses画面制御が終了したかを返す。
cols
     画面に表示可能な桁数を返す。
lines
     画面に表示可能な行数を返す。cols
     とともに利用して,想定する処理を行なうのに必要な画面サイズが
     あるかあらかじめ確認しておくのが望ましい。
     例:
require 'curses'
include Curses
if lines < 25 then
  STDERR.puts("端末を25行以上にしてから起動してください")
  exit 1
elsif cols < 80
  STDERR.puts("端末を桁幅80桁以上にしてから起動してください")
  exit 1
end
     doupdate
     refreshよりも効率的に画面更新を行なう。
getstr
     文字列を読み込みその値を返す。
delch
     カーソル位置の1バイト分の文字を削除して詰める。
insch(ch)
     カーソル位置の1バイト分の文字 ch を挿入する。
delch と insch を用いた例題プログラム
     cur-insdel.rb
     (後掲)を参照せよ。
deleteln
     カーソル位置の行を削除し1行分詰める。
insertlnカーソル位置に1行空行を追加。deleteln
     と insertln を利用した例題プログラム
     cur-line.rb
     (後掲)を参照せよ。
maxx, maxy, curx, cury
     それぞれそのCurses::Windowオブジェクトの 桁数,行数,現在のカーソル位置のカラム番号, 現在のカーソル位置の行番号,を返す。
inch
     そのウィンドウの現在のカーソル位置に表示されている文字 (1バイト)の文字コードを返す。
#!/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
#!/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を用いたアクション型プログラムの例を示す。
#!/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
実際に動かすと,●記号が左から右に向かって進み, スペースキーを押すとジャンプする。 スペースキーを押した直後の画面の様子を以下に示す。