昨日はウィンドウの位置と移動について説明した。
今日は文字の入力について。
文字の入力と入力モード
通常、文字の入力はバッファリングされ、Enterキーを押された時点で初めてユーザプログラムにはデータが渡される。
しかし、それではCUIアプリケーションを作るには不都合。
そこで、Cursesでは入力モードが用意されてる。
入力モードには、以下の3つがある:
- cookedモード
- cbreakモード
- rawモード
cookedモード
通常の入力と同じように、バッファリングを行う状態。
Enterキーが押されるまでは、入力されたデータはユーザプログラムに渡ってこない。
cbreakモード
バッファリングを行わない状態。
Curses.#getch()
モジュール関数などを呼んだときに、入力された文字は即座にユーザプログラムに渡される。
rawモード
バッファリングを行わず、さらに通常はshellで解釈される特殊な文字(Ctrl+CやCtrl+Zなど)をユーザプログラム側で扱えるようにする。
このモードの場合、Ctrl+Cを押してもプログラムは強制終了しなくなるので、ユーザプログラム側で適切にハンドルする必要がある。
入力モードの切り替え
入力モードを切り替えるには、以下のモジュール関数を使用する:
Curses.#nocbreak()
Curses.#nocrmode()
Curses.#noraw()
入力モードをcookedモードにする。
Curses.#cbreak()
Curses.#crmode()
入力モードをcbreakモードにする。
Curses.#raw()
入力モードをrawモードにする。
入力の取得
ユーザからの入力を得るには、以下のメソッドを使う:
Curses::Window#getch()
ユーザの入力から一文字読み込んで返す。
Curses::Window#getstr()
ユーザの入力から一行読み込んで返す。
(cbreakモードやrawモードでも、Enterキーが押されて一行になるまではブロッキングされる)
関連するメソッドで、以下のようなものもある:
Curses::Window#ungetch(ch)
文字ch
をバッファの先頭に戻す。
戻せるのは一文字まで。
Curses::Window#inch()
カーソル位置の文字を読み込んで返す。
なお、カーソルキーやファンクションキーの入力を受けたい場合、そのままでは扱えない。
その場合、以下のメソッドを呼び出しておく必要がある:
Curses::Window#keypad(bool)
bool
がtrue
の場合、キーパッドが有効になる。
すなわち、カーソルキーやファンクションキーの入力が、Curses::KEY_*
(あるいはCurses::Key::*
)という定数で返ってくるようになる。
(これらの定数については、RDocなどを参照)
あと、マルチバイト文字には対応していないみたい・・・
エコーバックとカーソルの表示
ユーザからの入力を得るメソッドがウィンドウのインスタンスメソッドになっているのは、入力のエコーバックの関係。
ユーザが文字を入力すると、基本的にはウィンドウのカーソルの位置にエコーバックされる。
エコーバックするかどうかは、以下のモジュール関数で設定することが出来る:
Curses.#echo()
入力のエコーバックを有効にする。
Curses.#noecho()
入力のエコーバックを無効にする。
また、次のモジュール関数を使うことで、カーソルの表示/非表示を切り替えることも出来る:
Curses.#curs_set(visibility)
visibility
が0の場合、カーソルが非表示になる。
visibility
が1の場合、カーソルが表示される。
入力待ちのタイムアウト
getch
などでは、ユーザから入力があるまでプログラムは停止して待つことになる。
この状態をブロッキングと呼ぶ。
一方、プログラムは停止せず、入力がなかった場合にはエラーを返して戻る状態をノンブロッキングと呼ぶ。
ユーザから入力がないときにいつまでも待ち続けてしまうと困る場合、入力待ちのタイムアウトが必要になってくる。
入力待ちのタイムアウトを設定するには、次のメソッドを使う:
Curses::Window#timeout=(val)
ウィンドウの入力待ちについて、タイムアウトを設定する。
上記のメソッドの場合、タイムアウトするまでの時間を設定しているので、それよりも前に入力があった場合には即座にユーザプログラムに制御が戻ってくる。
ただ、場合によっては、ユーザからの入力があってもなくても常に一定時間待って、そのあと処理を行いたいということもあるかと思う。
そういった場合には、timeoutライブラリを使うといい。
timeoutライブラリを読み込むと、KernelモジュールにKernel#timeout
メソッドが追加で定義される。
このtimeout
メソッドは、KernelモジュールがObjectクラスにincludeされているので、任意の場所から関数のように呼び出すことが出来る。
Kernel#timeout(sec) {|i| ...}
ブロックの内容を最大sec
秒実行する。
sec
には小数も指定出来る。
指定された時間を越えた場合、TimeoutError
例外が発生し、ブロックを抜ける。
例えば、次のようにすることで、常に一定時間待つようにすることが出来る:
input = nil begin timeout(待つ秒数) do input = Curses.getch sleep end rescue TimeoutError # ignore end
ユーザから入力を受け付けたり、入力待ちのタイムアウトの例として、次のコードを書いてみた:
#==================== # input_dialog.rb #-------------------- # 文字入力のサンプル #==================== require 'curses' require 'timeout' class Curses::Window def touch self.move(self.begy, self.begx) end def touch_noutrefresh self.touch self.noutrefresh end end Curses.init_screen Curses.cbreak screen = Curses.stdscr class << screen def show_message(message) self.setpos(0, 0) self.clrtoeol self.addstr(message) end end prompt_message = "Input any string!" input_dialog = Curses::Window.new( 4, prompt_message.size + 4, (Curses.lines - 4) / 2, (Curses.cols - prompt_message.size - 4) /2) input_dialog.box('|', '-') input_dialog.setpos(1, 2) input_dialog.addstr(prompt_message) class << input_dialog def get_input self.setpos(2, 2) Curses.echo Curses.curs_set(1) str = self.getstr if (str == "exit") || (str == "quit") str = nil else # clear input string. # (and recover box.) self.setpos(2, 2) self.clrtoeol self.box('|', '-') end Curses.noecho Curses.curs_set(0) return str end end # main loop loop do screen.show_message("Input 'exit' or 'quit' to exit.") screen.touch_noutrefresh input_dialog.touch_noutrefresh Curses.doupdate input = input_dialog.get_input if input.nil? break end screen.show_message("Input 'q' to change string.") word_win = Curses::Window.new( 1, input.size, 0, (Curses.cols - input.size) / 2) word_win.addstr(input) line = 0 speed = 0 acc = 0.4 rate = - 0.8 done = false until(done) screen.touch_noutrefresh word_win.noutrefresh Curses.doupdate begin timeout(0.05) do ch = Curses.getch if ch == "q" done = true end sleep end rescue TimeoutError # ignore end line = (line + speed).ceil speed += acc if (line >= Curses.lines) line = Curses.lines - 1 speed *= rate end word_win.move(line, word_win.begx) end word_win.close end Curses.close_screen
実行すると、文字列の入力が促される。
このとき、'exit'もしくは'quit'と入力してEnterキーを押すと、プログラムは終了。
それ以外の文字列が入力された場合、その文字列が画面内でバウンドして表示される。
その最中に'q'が入力されると文字列のバウンドはすぐに終わり、新しい文字列の入力が促される。
このコードについて少し説明すると、以下の通り:
まず、42〜63行目でやっているのは、ユーザからの入力を受け取るための特異メソッドの定義。
このとき、
としている。
そして、エコーバックがあると、カーソル位置の文字が上書きされてしまう。
そこで、ユーザの入力が終わったら、上書きされた部分をクリアしている。
また、ユーザの入力が長かった場合、ウィンドウの枠が上書きされてしまうことがあるので、枠の修復も行っている。
そして、78〜113行目では、ユーザから入力された文字列を、画面内でバウンドするように描画している。
このとき、描画は一定間隔毎に行う必要があるので、ユーザからの入力があるかないかに関わらず、一定時間入力待ちをするようにしている。
(そうしないと、キーを押しっぱなしにされた場合、描画間隔が短くなってしまう)
今日はここまで!