読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

Curses for Ruby。(その4)

技術 Ruby 端末 Curses

昨日はウィンドウの生成・削除と描画について説明した。

今日はウィンドウの位置と移動について説明する。

ウィンドウの位置と移動

ウィンドウは、位置を変えたりサイズを変更したりすることが可能。

まず、ウィンドウの位置を取得するには、以下のメソッドが使える:

Curses::Window#begy()
ウィンドウの左上が、画面の何行目にあるかを返す。
(画面の左上が0行目)

Curses::Window#begx()
ウィンドウの左上が、画面の何列目にあるかを返す。
(画面の左上が0列目)

ウィンドウのサイズを取得するには、以下のメソッドが使える:

Curses::Window#maxy()
ウィンドウの行数を返す。

Curses::Window#maxx()
ウィンドウの列数を返す。

ウィンドウの位置を変えるには、次のメソッドを使う:

Curses::Window#move(top, left)
ウィンドウの左上がtopleft列目になるように移動する。
(画面の左上が0行0列目)

そして、ウィンドウのサイズを変えるには、次のメソッドを使う:

Curses::Window#resize(height, width)
ウィンドウのサイズをheightwidth列に変更する。

ウィンドウのタッチ更新

ところが、実際にウィンドウの移動を行ってみると、不思議なことが起こる。

例えば、次のコード。

#====================
# bad_move_1.rb
#--------------------
# ウィンドウの移動が上手くいかない例(その1)
#====================

require 'curses'

Curses.init_screen
Curses.cbreak
Curses.stdscr.refresh

win = Curses::Window.new(5, 10, 0, 0)
win.setpos(2, 4)
win.addstr("Hi")
win.box('|', '-')

win.refresh
Curses.getch

win.move(2, 2)
win.refresh
Curses.getch

Curses.close_screen

やろうとしているのは、ウィンドウを生成して画面に表示したあと、ウィンドウを移動して再度表示させるということ。
けど、実際にやってみると、移動前のウィンドウが画面に残ってしまう。

これは、ウィンドウそのものが画面に表示されるわけではなく、仮想画面の内容が画面に表示されるのが原因。
どういうことかというと、

  1. 最初にウィンドウの内容を仮想画面に反映して画面に表示したときには、仮想画面にウィンドウが0行0列目にある状態が書き込まれる。
  2. そのあと、ウィンドウを2行2列目に移動して仮想画面に反映したときには、仮想画面にウィンドウが2行2列目にある状態が書き込まれる。

となっているので、元々の0行0列目にあるウィンドウのイメージは仮想画面に残ったままになってしまうから。

それなら、まずは画面全体を覆うデフォルトのウィンドウを仮想画面に反映し、そのあと移動後のウィンドウを仮想画面に反映すれば、大丈夫そうに思える。
そうすれば、デフォルトのウィンドウを仮想画面に反映した時点で仮想画面全体がまっさらになり、問題なく移動後のウィンドウのみが表示されるはずなので。

ということで、変更したコードが以下:

#====================
# bad_move_2.rb
#--------------------
# ウィンドウの移動が上手くいかない例(その2)
#====================

require 'curses'

Curses.init_screen
Curses.cbreak
Curses.stdscr.refresh

win = Curses::Window.new(5, 10, 0, 0)
win.setpos(2, 4)
win.addstr("Hi")
win.box('|', '-')

win.refresh
Curses.getch

Curses.stdscr.refresh  # デフォルトのウィンドウを仮想画面に反映させる
win.move(2, 2)
win.refresh
Curses.getch

Curses.close_screen

けど、実際に動かしてみると、結果はさっきと変わらず。
移動前のウィンドウが画面に残ってしまう。

原因は、noutrefresh(もしくはrefresh)は「ウィンドウに対する変更」を仮想画面(と画面)に反映するので、ウィンドウに何も変更が加わっていない場合、何も仕事をしないから。
今回の場合、本当はデフォルトのウィンドウ全体を仮想画面に反映したいんだけど、デフォルトのウィンドウには何も変更が加えられていないので、refreshをしても仮想画面には何も影響が出ない。
なので、相変わらず移動前のウィンドウが画面に残ってしまう。

これを解決するには、いくつかの方法がある。

例えば、デフォルトのウィンドウをクリアしてあげれば、デフォルトのウィンドウ全体が更新されたという扱いになるので、デフォルトのウィンドウ全体が仮想画面に反映されて、移動前のウィンドウは表示されなくなる。
あるいは、移動前のウィンドウを一度クリアして仮想画面に反映したあと、移動してから再度ウィンドウの内容を作って仮想画面に反映するという方法も考えられる。

けど、いずれの方法も、ウィンドウの内容を消したくない場合には不都合。

そこで使うのが、ウィンドウのタッチ更新
これをすることで、ウィンドウの内容を変えることなく、ウィンドウ全体を更新したと見做させることが出来る。
(ファイルの内容を変えることなくタイムスタンプだけを更新するtouchコマンドを想像すると分かりやすいと思う)

C言語の場合、touchwin()という関数が用意されているんだけど、cursesライブラリではタッチ更新のインタフェースが用意されていない。
そこで、ちょっとした工夫が必要になってくる。

具体的には、ウィンドウを「今の位置」に移動させることで、ウィンドウ全体を更新したと見做させることが出来る。
もちろん、今の位置に移動させるだけなので、ウィンドウの内容も位置も実際には変化しない。
けど、更新されたと見做されるので、noutrefreshrefreshによってウィンドウ全体の内容が仮想画面に反映されるようになる。

タッチ更新を行うようにしたコードが、以下:

#====================
# good_move.rb
#--------------------
# ウィンドウの移動
#====================

require 'curses'

Curses.init_screen
Curses.cbreak

screen = Curses.stdscr
screen.refresh

win = Curses::Window.new(5, 10, 0, 0)
win.setpos(2, 4)
win.addstr("Hi")
win.box('|', '-')

win.refresh
Curses.getch

screen.move(screen.begy, screen.begx)  # タッチ更新
screen.refresh
win.move(2, 2)
win.refresh
Curses.getch

Curses.close_screen

これで、移動前のウィンドウが画面に残らなくなる。

実際には、次のようにオープンクラスを使ってCurses::WindowクラスにCurses::Window#touchというメソッドを定義しておくといいと思う:

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

今日はここまで!