いものやま。

雑多な知識の寄せ集め

Curses for Ruby。(その3)

昨日はcursesライブラリの基本的な内容について説明した。

今日はウィンドウの生成・削除と描画について。

ウィンドウの生成・削除

ウィンドウは、画面に出力する文字情報を持った矩形の領域。
Cursesでは、ウィンドウに対して文字を追加・削除する操作を行い、その変更を仮想画面、そして画面へと反映させていく。

Cursesではウィンドウを複数作ることが出来る。
また、初期化した段階で、画面全体を覆うデフォルトのウィンドウが作られている。
Cursesモジュールのモジュール関数のいくつかは、このデフォルトのウィンドウを対象としたものとなっている。

デフォルトのウィンドウを取得するには、以下のモジュール関数を使う:

Curses.#stdscr()
画面全体を覆うデフォルトのウィンドウを返す。 デフォルトのウィンドウは、Curses::Windowクラスのインスタンス

また、ウィンドウを新しく生成したい場合、次のクラスメソッドを使う:

Curses::Window.new(height, width, top, left)
高さがheight行、幅がwidth列で、左上の位置が画面のtop行目、left列目であるような新しいウィンドウが生成し、そのインスタンスを返す。
(画面の左上が0行0列目)

生成したウィンドウを削除するには、以下のメソッドを使う:

Curses::Window#close()
ウィンドウを削除し、メモリを解放する。

ウィンドウへの文字の追加・削除

各ウィンドウにはカーソルがあり、その位置に対して文字を追加・削除することが出来る。

ウィンドウ内でのカーソル位置を知るには、以下のメソッドを使う:

Curses::Window#cury()
カーソルがウィンドウの何行目にあるかを返す。
(ウィンドウの左上が0行目)

Curses::Window#curx()
カーソルがウィンドウの何列目にあるかを返す。
(ウィンドウの左上が0列目)

カーソル位置を移動するには、次のメソッドを使う:

Curses::Window#setpos(y, x)
カーソル位置をウィンドウのyx列目にする。
(ウィンドウの左上が0行0列目)

ウィンドウに文字を追加するには、以下のメソッドを使うことが出来る:

Curses::Window#addch(ch)
ウィンドウのカーソル位置に文字chを上書きし、カーソルを進める。

Curses::Window#addstr(str)
Curses::Window#<<(str)
ウィンドウのカーソル位置に文字列strを上書きし、カーソルを進める。

Curses::Window#insch(ch)
ウィンドウのカーソル位置に文字chを挿入する。

Curses::Window#insertln()
ウィンドウのカーソル位置に一行挿入する。

ウィンドウの文字を削除するには、以下のメソッドを使うことが出来る:

Curses::Window#delch()
ウィンドウのカーソル位置の文字を削除する。
(以降の文字は前に詰められる)

Curses::Window#deleteln()
ウィンドウのカーソル位置の行を削除する。
(以降の行は前に詰められる)

Curses::Window#clear()
ウィンドウの内容をすべて削除する。

Curses::Window#clrtoeol()
ウィンドウのカーソル位置から行末までを削除する。

ウィンドウには、枠をつけることも可能。

Curses::Window#box(vert, hor)
ウィンドウの矩形領域の一番外側を枠で囲う。
このとき、垂直方向には文字vert、水平方向には文字horが使われる。

ウィンドウに枠をつけるときに気をつけたいのが、この枠は矩形領域の外側につけられるわけではなく、矩形領域の内側につけられるということ。
このため、枠を作る文字の下にすでに文字があると上書きされてしまうし、逆に、枠のある位置に文字を上書きすると、枠を作る文字が消えてしまうことになる。

ウィンドウの変更の画面への反映

ここまでで、ウィンドウに対して文字の追加や削除を行う方法を説明した。
けど、これらの変更は、それだけでは画面には反映されない。
そこで、次はウィンドウに対する変更を画面に反映する方法について説明する。

ウィンドウの変更を仮想画面(と画面)へ反映するには、以下のメソッドを使う:

Curses::Window#noutrefresh()
ウィンドウの変更を仮想画面へ反映する。
(画面へは反映されない)

Curses::Window#refresh()
ウィンドウの変更を仮想画面と画面へ反映する。

Curses::Window#noutrefreshメソッドを使った場合、仮想画面の内容を画面に反映する必要がある。
その場合、次のモジュール関数を使う:

Curses.#doupdate()
仮想画面の内容を画面へ反映する。

ウィンドウが一つしかない場合、refreshを使うのとnoutrefresh + doupdateを使うのは同じだけど、ウィンドウが複数あった場合、効率が変わってくる。
一般に、デバイスへのアクセスはメモリへのアクセスより遅いため、頻繁にデバイスへアクセスするのは効率が悪くなる。
そのため、複数のウィンドウを使う場合、各ウィンドウに対してnoutrefreshをしたあとで最後に一度だけdoupdateをする方が、効率がよくなる。

なお、複数のウィンドウがある場合、Cursesでは各ウィンドウがどの順番に重なっているのかは管理していない。
各ウィンドウの変更内容は、noutrefresh(もしくはrefresh)された順に仮想画面へ上書きされていく。
そのため、見かけ上は、あとでnoutrefreshされたウィンドウの方が上に重なっているように見える。


ここまでの内容の確認と、refresh / noutrefresh + doupdateの違いを見るために、次のコードを見てみる:

#====================
# multi_window.rb
#--------------------
# refresh / noutrefresh + doupdateの違いを確認する
#====================

require 'curses'

class Curses::Window
  # タッチ更新
  def touch
    self.move(self.begy, self.begx)
  end
end

Curses.init_screen
Curses.cbreak

screen = Curses.stdscr

message = "Hello"
windows = Array.new
10.times do |i|
  win = Curses::Window.new(3, 10, i+1, i+1)
  win.setpos(1, (10 - message.size) / 2)
  win.addstr(message)
  win.box('|', '-')
  windows.push win
end

# refresh

screen.setpos(0, 0)
screen.addstr("refresh")
screen.refresh
Curses.getch

windows.each do |win|
  win.refresh
  sleep 0.1
end
Curses.getch

# noutrefresh + doupdate

screen.clear
screen.setpos(0, 0)
screen.addstr("noutrefresh + doupdate")
screen.refresh
Curses.getch

windows.each do |win|
  win.touch
  win.noutrefresh
  sleep 0.1
end
Curses.doupdate
Curses.getch

windows.map(&:close)
Curses.close_screen

このコードを実行すると、次のような結果になる:

  1. 画面左上に"refresh"と表示されて、入力待ちになる。
  2. 何かキーが押されると、少しずつ位置のずれたウィンドウが0.1秒間隔で表示されていき、最後に入力待ちになる。
  3. 何かキーが押されると、画面がクリアされる。
  4. 画面左上に"noutrefresh + doupdate"と表示され、入力待ちになる。
  5. 何かキーが押されると、しばらく経った後に、少しずつ位置のずれたウィンドウがすべて表示され、入力待ちになる。
  6. 何かキーが押されると、プログラムが終了する。

この結果から、次のことが確認できると思う:

  • refreshを行うと、そのたびに画面に変更が反映される。
  • noutrefreshしただけでは、画面に変更は反映されない。
  • doupdateしたとき、それまでの変更がすべて画面に反映される。
  • あとからrefreshもしくはnoutrefreshされたウィンドウが上に重なっているように見える。

なお、コードの最初の方でオープンクラスを使ってtouchというメソッドを追加しているけど、これについてはまた後で。

今日はここまで!