これまで作成したプログラムを利用する場合,利用者がプログラムに対して 行なうアクションは「文字列を入力して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
実際に動かすと,●記号が左から右に向かって進み, スペースキーを押すとジャンプする。 スペースキーを押した直後の画面の様子を以下に示す。