画面とキー入力制御の自由度を高める curses ライブラリを用いると、高機能な対話的プログラムが作れる。
cursesをプログラム内に導入し、利用宣言すると以下のことが可能になる。
その一方、画面出力・キー入力がすべてcursesのものになるため、 普通に標準入出力を用いているプログラムで可能な以下のことが できなくなる。
puts, print, printf
などを使った文字列出力(期待どおりの場所に出ない)
出力が画面下端に行った場合の画面の自動スクロール (最下行で止まる)
getsなどの入力関数(データを送れない)
これらのことに気をつけ、それぞれのメソッドをcurses専用の ものを必ず利用するように気をつけていれば問題ない。
cursesを利用したプログラムは以下のような流れで作成する。
#!/usr/koeki/bin/ruby require 'curses' include Curses init_screen # スクリーンを初期化する begin 〜 必要な処理本体 〜 ensure close_screen # スクリーンを元に戻す end
エスケープシーケンス (基礎プログラミングI) を用いても画面の任意の位置に文字出力できるが、位置決めを頻繁に行なう ようなプログラムではcursesを用いる方がプログラムを作りやすい。
たとえば、画面上「5行目、10桁目」に「こんにちは」と表示する プログラムは以下のようになる。
#!/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キーを押すまで進まない:
#!/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キーを押さなくても進む:
#!/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 とする。
#!/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 を利用した例を示す。
#!/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 |
mのnビット右シフト |
m >> n |
mのnビット左シフト |
cursesが統轄する画面は Curses::Windowというクラス
のオブジェクトで、最初のウィンドウは stdscr
というオブジェクトである。これまで説明したメソッドはすべて
stdscr に対する処理を行なうためのものである。
cursesでは stdscr とは別の新たなウィンドウを生成して
そのウィンドウだけに処理を及ぼすことができる。プルダウンメニューや
会話的出力の箱型ウィンドウなどはサブウィンドウを用いて作成する。
サブウィンドウは subwin メソッドで生成する。
Curses::Window オブジェクトを返すのでその値を
保存しておき、サブウィンドウ内への出力を行ないたい場合に
それを利用する。
#!/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-p、C-n、C-b、C-f や、
k、j、h、l にも
割り当てる。前者はEmacsの、後者はviの標準キー割り当てなので
説明なしでもとっさに使いこなしてもらえる可能性が高い。
矢印キーでマークを移動できる
プログラムの例を cur-keypad.rb
に示す。
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バイト)の文字コードを返す。
cursesを用いたアクション型プログラムの例を示す。
#!/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