端末とは,コンピュータの基本的な機能 (キーボードから命令を受け取り,画面に文字を描く機能) を実現するためのハードウェアやソフトウェアのことである.
これまでのC言語プログラムでは,標準ライブラリの入出力関数 printf( ) と scanf( ) 等を利用して, 端末の入出力を実行してきた. しかし,これらの関数では,一行単位でしか入出力できないし, 白黒でしか表示できない,等の制約が多かった. 「なーんだC言語では,まったく面白味のないプログラムしか作れないのかー」 いや,それは誤解だ.
端末自身は,キーボードや画面をより自由自在に制御する機能をもっており, C言語の標準ライブラリでも,それらの機能の一部を利用できる. さらに,curses ライブラリを利用すれば, エディタやビデオゲームのような高機能な端末制御プログラムを 効率的に作成できるようになる.
端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするには, エスケープシーケンス(escape sequence) と呼ばれる特殊な文字列を利用する.
たとえば,次のようなコマンドを実行すると, 文字の色を変更できる:
$ echo "^[[31m Red" # '^['の入力方法は下記の通り
Red
$ echo "^[[0m Reset"
Reset
$
$ PS1="$ "
ただし,文字列の先頭の記号 '^[' は, [Esc] キーを表わす特殊な文字である. 2 文字の文字列 "^[" ではない. この文字をコマンドラインで入力するためには, 次のように操作する:
これら Esc 文字から始まる文字列 ( "^[[31m" や "^[[0m") がエスケープシーケンスである. 代表的なエスケープシーケンスを Table 1 に紹介しておく. なお,この表では,エスケープ文字を「Esc」と表記している.
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
これらのエスケープシーケンスは,端末自身がもっている機能なので, 端末の上で動くものであれば,Cプログラムでも利用できる. 次のように, エスケープシーケンスを printf( ) 関数などで出力するだけだ.
...
printf("クリアして \e[2J");
printf("\e[3;0H 3行目で \e[31m 赤にしたり\n");
printf("\e[1;10H 1行目へ戻ってみたり\n");
printf("\e[5;0H\e[0m 5行目で元通りに\n");
...
ここで '\e' は Esc を表わす特殊文字である. 改行記号 '\n' などと同様,2文字で1文字を表わす.
実行例:
... $ ./a.out |
→ |
1行目へ戻ってみたり
3行目で赤にしたり
5行目で元通りに
$
|
エスケープシーケンスの文字列は, 人間が読んでもほとんど意味不明な「呪文(curse)」のようなものであり, それを直接利用してプログラムを作成するなんてことは非常に面倒だ. そこで,この面倒な処理を簡単化するための関数群が curses ライブラリとして用意されている.
このライブラリの関数を利用すると, 端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするプログラム (いわゆるフルスクリーンアプリケーション) が簡単に実現可能になる.
単純な curses プログラムの例を List 0 に示す. これをコンパイルするには,-lncurses オプションを指定し, ライブラリをリンクする必要がある:
$ cc hello.c -lncurses -o hello
実行すると,メッセージが表示され,何かキーを押すと終了する.
#include <ncurses.h>
int main()
{
initscr(); // 端末制御の開始
start_color(); // カラーの設定
init_pair(1, COLOR_RED, COLOR_BLUE); // 色番号1を赤文字/青地とする
bkgd(COLOR_PAIR(1)); // 色1をデフォルト色とする
erase(); // 画面表示
move(10, 20);
addstr("Hello World");
refresh();
timeout(-1);
getch(); // キー入力
endwin(); // 端末制御の終了
return (0);
}
List 0 のソースを読んでみよう. これまでのプログラムとは全然違うものに見えるだろう. とりあえず今は,細かいことについては,気にしないでよい.
まず,おなじみのヘッダファイル stdio.h とか 標準ライブラリの入出力関数 printf( ) や scanf( ) とかは, まったく使われていない. その代わり,ncurses.h をインクルードし, curses ライブラリの関数だけを使っている.
画面表示やキー入力のために, 標準ライブラリの標準入出力関数(printf() 等)と curses ライブラリの端末入出力関数(addstr( ) 等)とを 混ぜて使わないこと. 一緒に使ってもエラーにはならないが, 多分,期待した結果にはならない. 注意せよ.
ま,curses の入出力関数があれば, 標準入出力関数を使う必要はないので, 心配する必要もない. 端末入出力には,curses の関数だけを使うこと. ただし,ファイルの入出力の場合には, 端末制御とは無関係なので, 標準ライブラリの入出力関数(fprintf( ) 等)のお世話にもなる.
curses の関数についての詳しくは, 参考資料を参照せよ.
次のセクションから, curses ライブラリの使い方を段階的に学習して行く. ある程度まで理解できたら,自由に改造してみよう.
最も単純な curses プログラムを List 1 に示す.
#include <ncurses.h>
int main()
{
initscr();
move(10, 20);
addstr("Hello World");
getch();
endwin();
return (0);
}
これは,端末画面の上から 10 行目,左から 20 桁目の位置に 文字列 "Hello World" を表示するだけのものであり, 何かキーを押すと終了する.
ここで利用されている関数の意味は次の通り:
なお,画面表示の関数としては, addstr() の他に,次のようなものもある:
List 1 を元にして, 文字列を画面の中央に表示するように改良したものが List 2 である.
#include <ncurses.h>
#include <string.h>
int main()
{
int x, y, w, h;
char *str = "Hello World";
initscr();
getmaxyx(stdscr, h, w);
y = h/2;
x = (w - strlen(str))/2;
move(y, x);
addstr(str);
getch();
endwin();
return (0);
}
まず,一度,実行してみよう. そして,端末ウィンドウのサイズを変更してから, 再度,実行してみよう. サイズに応じて,表示位置が自動的に調整されるハズだ.
次の関数が登場した:
ここで,stdscr は端末画面を表わす定数である. 標準出力 stdout の curses 版だと考えればよい.
ところで,Cの基本がわかっている人は疑問に思うハズだ. なぜ,利用方法が getmaxyx(stdscr, &h, &w) ではなく, getmaxyx(stdscr, h, w) なのか?
それは,getmaxyx( ) が実は,関数ではなくマクロであるからだ. 次のような感じに引数付きマクロとして定義されている:
#define getmaxyx(画面, y, x) { y=画面の行数; x=画面の桁数; }
ポインタがわからないドシロートさんでもわかるような親切設計? しかし,Cについて生半可な知識があると,かえって混乱してしまいそうだ. 注意せよ. (このマクロは良い設計なのだろうか?良い子はマネしないこと!)
次に,何かキーを押すたびに表示位置を変化するようにしたものが List 3 である. List 2 の大改造になる. [q] キーを押すと終了する.
#include <ncurses.h>
#include <string.h>
int main()
{
int x, y, w, h;
char *str="Hello World";
int key;
initscr();
// noecho(); // echo();
// cbreak(); // nocbreak();
getmaxyx(stdscr, h, w);
y = h/2;
x = (w - strlen(str))/2;
while (1) {
// erase();
move(y, x);
addstr(str);
// refresh();
key = getch();
if (key == 'q') break;
y++; if (y >= h) y = 0;
}
endwin();
return (0);
}
無効化(コメント化)されている部分(// 以降)を 有効化しないと(// を外さないと)うまく行かないだろう. まず,コメントのまま実行し,その後,有効化して実行し, 動作を比較してみよう.
次の関数が出てきた:
ここで,refresh() の意味がよくわからないかもしれない. curses では, 実際の表示用の画面(物理画面,実画面)の他に, 内部的な作業用の画面(論理画面,裏画面)を利用している. 通常,出力の処理は,論理画面だけに対して作用する. つまり,出力しただけでは,実際の画面上には表示されない(場合がある).
表示したい場合には,出力処理の後で,refresh() しよう. これで,論理画面の内容が物理画面へ確実にコピーされ, 実際に見えるようになるハズだ.
今度は,キー入力しなくても自動的に動くようにしてみよう. List 3 のコピーを作り,List 4 のように変更しよう. (また後で使うので,List 3 については書き換えずに残しておこう.)
#include <ncurses.h>
#include <string.h>
#include <unistd.h>
int main()
{
int x, y, w, h;
char *str="Hello World";
int key;
initscr();
noecho();
cbreak();
timeout(0);
getmaxyx(stdscr, h, w);
y = h/2;
x = (w - strlen(str))/2;
while (1) {
erase();
move(y, x);
addstr(str);
refresh();
key = getch();
if (key == 'q') break;
y++; if (y >= h) y = 0;
usleep(100000);
}
endwin();
return (0);
}
次の関数を追加しただけ:
List 3 では,下方向にしか移動できなかった. 今度は,カーソルキーの入力によって上下左右に移動できるようにする. List 3 を元にして,List 5 のように書き換えよう.
#include <ncurses.h>
#include <string.h>
int main()
{
int x, y, w, h;
char *str="Hello World";
int key;
initscr();
noecho();
cbreak();
keypad(stdscr, TRUE);
getmaxyx(stdscr, h, w);
y = h/2;
x = (w-strlen(str))/2;
while (1) {
erase();
move(y, x);
addstr(str);
refresh();
key = getch();
if (key == 'q') break;
switch (key) {
case KEY_UP: y--; break;
case KEY_DOWN: y++; break;
case KEY_LEFT: x--; break;
case KEY_RIGHT: x++; break;
}
}
endwin();
return (0);
}
新たな関数はひとつだけ:
さてここで, getch( ) の戻り値の型が char ではなく int だったことの理由を明かそう. ASCII コード(1バイトの数値,0x00 〜 0xFF) だけでは表わしきれない程に多数ある特殊キーにも 番号(4バイトの数値,0x00000100 〜 0xFFFFFFFF)が割り当てられている. また,各特殊キーには, わかり易いマクロ名も定義されている:
今度は,カラーを使ってみる. List 6 をゼロから作成しよう.
#include <ncurses.h>
int main()
{
// 端末の準備
initscr();
// 色の準備
start_color();
init_pair(1, COLOR_RED, COLOR_BLUE); // 色1 は青地に赤文字
init_pair(2, COLOR_GREEN, COLOR_BLUE); // 色2 は青地に緑文字
init_pair(3, COLOR_YELLOW, COLOR_BLUE); // 色3 は青地に黄文字
init_pair(10, COLOR_WHITE, COLOR_BLUE); // 色10 は青地に白文字
bkgd(COLOR_PAIR(10)); // 背景は色10
// 表示
attrset(COLOR_PAIR(1)); // 色1 を使う
mvaddstr(5, 5, "Hello World");
attrset(COLOR_PAIR(2)); // 色2 を使う
mvaddstr(5, 25, "Hello World");
attrset(COLOR_PAIR(3)); // 色3 を使う
mvaddstr(5, 45, "Hello World");
attrset(COLOR_PAIR(1) | A_BOLD); // 色&強調表示
mvaddstr(6, 5, "Hello World");
attrset(COLOR_PAIR(2) | A_BOLD);
mvaddstr(6, 25, "Hello World");
attrset(COLOR_PAIR(3) | A_BOLD);
mvaddstr(6, 45, "Hello World");
attrset(COLOR_PAIR(1) | A_REVERSE); // 色&反転表示
mvaddstr(7, 5, "Hello World");
attrset(COLOR_PAIR(2) | A_REVERSE);
mvaddstr(7, 25, "Hello World");
attrset(COLOR_PAIR(3) | A_REVERSE);
mvaddstr(7, 45, "Hello World");
attrset(COLOR_PAIR(1) | A_REVERSE | A_BOLD); // 色&反転&強調
mvaddstr(8, 5, "Hello World");
attrset(COLOR_PAIR(2) | A_REVERSE | A_BOLD);
mvaddstr(8, 25, "Hello World");
attrset(COLOR_PAIR(3) | A_REVERSE | A_BOLD);
mvaddstr(8, 45, "Hello World");
// 終了
getch();
endwin();
return (0);
}
端末の種類や設定によっては,うまく働かない機能もあるかもしれないが, あまり気にしないでおこう.
List 5 のプログラムで左右に移動しているとき, 文字列の一部が画面からハミ出すと表示が乱れてしまう. この対策を施せ.
ヒント:画面右側の範囲チェックでは, 文字列の末尾の座標を調べる必要があるだろう.
ヒント:addstr( ) ではなく, addch( ) または mvaddch( ) を利用するとよい. つまり,文字列を一度に出力するのではなく, 一文字ずつ位置を指定しながら出力するとよい. このとき,当然,各文字の出力位置が画面内に収まるようにすること.
全角日本語文字列(というかマルチバイト文字列)も利用できる:
#include <ncurses.h>
#include <locale.h>
int main(...)
{
setlocale(LC_ALL, "");
initscr();
...
addstr("こんにちWorld"); // 全角・半角の混合文字列
...
}
コンパイル方法: (ncurses ではなく,ncursesw ライブラリを使用)
$ cc ... -lncursesw
このように,文字列全体を表示するだけなら,とても簡単だ. しかし,全角文字では,2バイト以上で1文字を表現していたり, 画面上で半角2文字分の幅を使ったりするので, 練習問題2のように, 1文字ずつとか1バイトずつに分解して処理することは難しい.
無理ではない.しかし,文字コードに関する高度な知識が必要. したがって,とりあえず, 文字列分解処理が必要な場合には,全角文字列を使わない ってことで妥協しよう. たとえば,今回の練習問題2では, 文字列を日本語に変えると動作保証対象外としてよい.
上級者向け情報: ワイド文字(日本語文字というか英語以外の文字)用のデータ型 wchar_t, マルチバイト列(Cの標準の文字列)とワイド文字列の変換関数 mbtowc(), etc. で検索してみよう.