main() { int i, j; char buffer[128]; puts("number?"); gets(buffer); i = atoi(buffer); puts("number?"); gets(buffer); j = atoi(buffer); printf("%d - %d = %d\n", i, j, i - j); }
引かれる数と引く数を順に受け取って、差を表示すればよい。
ところがこの単純なプログラムも、GUI ベースにしたとたんに複雑化する。 GUI ベースのプログラムでは、つぎのようなウィンドウを表示することになろう。
ユーザは引く数と引かれる数、どちらを先に入力してもよい。 またプログラムはユーザが OK ボタンをクリックしたときに終了する。
GUI ベースのプログラムの難しさは、ひとことで言って、ユーザからの さまざまな種類の入力を同時に待ち受けなければならない点にある。 上の例では、キーボード入力だけでなく、マウスの移動、ボタンの操作も同時に調べなければならない。 また、引かれる数と引く数のどちらを入力するかは、それに先だってどちらのフィールドをクリックしたかで決まる。 したがってプログラムはユーザによる操作の履歴も管理しなければならない。
このような複雑な処理をおこなわなければならない GUI ベースのプログラムは、普通、イベント駆動型と呼ばれるスタイルで書かれる。 このスタイルは次のような形をしており、ちょうとコンパイラの字句解析部と同様の形をしている。
main() { 初期設定。 for (;;){ e = 次のイベントが発生するまで待ち、発生したらそのイベントを返す。 switch(e) { case キー入力: キー入力の処理。 break; case マウスの移動: マウスの移動の処理。 break; case ... } } }
イベントとは、ユーザからのさまざまな入力のことである。 ユーザがキーを押したり、マウスを動かしたり、あるいはマウスボタンを押し下げたり、離したりする個々の動作をイベントと呼ぶ。 プログラムは初期設定の後、無限ループに入り、何かイベントが発生するまで待ち、発生したら発生したイベントに応じた処理をおこなう、ということを繰り返す。
プログラム例として、画面上にウィンドウをひとつ生成するプログラム simple.c を用意した。 このプログラムはウィンドウ内にボタンをひとつ表示し、ボタンが押されたら終了する。
このプログラムは5つの関数からなる。 まず main() の内容から解説する。
void main(int argc, char** argv) { : if ((display = XOpenDisplay(NULL)) == NULL) { fprintf(stderr, "cannot open display\n"); exit(1); } screen = DefaultScreen(display);
はじめは初期設定である。 X Windows は任意のマシンの画面上にウィンドウを生成できるので、まず、どのマシンの画面にウィンドウを生成するか選択する。 XOpenDisplay() がそのための関数で、引数に NULL を与えると、同一マシンの画面か、あるいは環境変数 DISPLAY で指定された画面を選択する。 次の screen は、選択したマシンが複数のスクリーンを持っていた場合、どのスクリーンにウィンドウを生成するかを指定する識別子である。 通常、スクリーンはひとつしかないので、この識別子はあまり意味をもたない。
win = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 300, 200, 1, BlackPixel(display, screen), WhitePixel(display, screen));
次にウィンドウを生成する。 注意すべきは、この関数を呼んでもすぐに画面上にウィンドウが現れるわけではないことである。 XCreateSimpleWindow() の引数は、順に、どの画面上に生成するか、親ウィンドウ、x 座標、y 座標、幅、高さ、ウィンドウの枠の太さと色、そしてウィンドウの背景色である。 X Window では、個々のウィンドウは必ず別なウィンドウの子ウィンドウであり、画面の背景をあらわす root ウィンドウを頂点とする親子関係を全体として作る。 他のウィンドウから独立したウィンドウを作るには、root ウィンドウを親ウィンドウとして指定すればよい。
icon = XCreateBitmapFromData(display, win, teacup_bits, teacup_width, teacup_height); XSetStandardProperties(display, win, "Simple", "tea time", icon, argv, argc, NULL); XSelectInput(display, win, ExposureMask | ButtonPressMask | ButtonReleaseMask);
次に生成したウィンドウの属性をいくつか指定する。 上の例では、まずウィンドウをアイコン化したときに使うアイコンのビットマップを生成し、それをウィンドウのタイトルとともにウィンドウの属性として指定している。 アイコンのビットマップ・イメージを作るための元データは teacup.bitmap というファイルの中にあり、simple.c の最初で #include されている。 元データは X Window のビットマップ・エディタ bitmap で作成した。 最後にこのウィンドウが受け取るイベントの種類を指定している。 このウィンドウは、再描画 (Expose)、マウス・ボタンを押し下げる動作、離す動作をイベントとして受け取る。
XMapWindow(display, win); gc = GetGraphicsContext(display, screen, win, "9x15");初期化処理の最後に XMapWindow() を呼んで、生成したウィンドウを画面上に貼り付ける。 この処理によってウィンドウは実際に画面上に表示されるようになる (正確には次に XNextEvent() を呼ぶまで表示されない)。 またグラフィック・コンテキスト gc を用意する。 グラフィック・コンテキストとは、描画の際に用いられる架空のペンのようなものである。 XDrawLine() のような描画関数は、すべてグラフィック・コンテキストを引数にとり、この中で指定されている色やフォント等にしたがって描画をおこなう。 GetGraphicsContext() は simple.c 中で定義されている関数である。 詳細は関数の定義を見てほしい。
イベント駆動型プログラミングで重要な点は、ウィンドウの再描画のタイミングもイベントとして通知されることである。 X Window のようにマルチウィンドウのシステムでは、他のプログラムの動作やウィンドウ・マネージャによる操作によって、ウィンドウの一部や全部が隠れたり表にでたりする。 このため X Window は必要に応じて再描画 (Expose) イベントをプログラムに送り、再描画をうながす。 プログラムは再描画イベントを受け取ったときには、すぐにウィンドウを描画しなければならない。 さもないとウィンドウの一部が白い (背景色の) ままになってしまう。
button_pressed = false; for(;;) { XNextEvent(display, &event); switch (event.type) { case Expose : while (XCheckTypedEvent(display, Expose, &event)) ; Redraw(display, win, gc, button_pressed); break; case ButtonPress : button_pressed = OnButton(&event); ClearAndRedraw(display, win, gc, button_pressed); break; case ButtonRelease : button_pressed = false; MouseClick(display, win, &event); ClearAndRedraw(display, win, gc, button_pressed); break; default : break; } }
はじめの変数 button_pressed は、現在のボタンの状態を表す状態変数である。 コンパイラの字句解析部と同様、イベント駆動型のプログラムの動作もオートマトンとしてとらえることができる。 例のプログラムでは、マウスのボタンを押し下げている間はボタンを反転表示し、ボタンが離されて初めてプログラムの終了処理をおこなう。 このためマウスのボタンが押し下げられている間は、ボタンの描画の方法を変えなければならない。 どちらの方法で描画するかを指定するのが button_pressed である。
case Expose : while (XCheckTypedEvent(display, Expose, &event)) ; Redraw(display, win, gc, button_pressed); break;
実は再描画イベントは、再描画が必要になった部分(矩形)ごとに送られる。 しかしこのプログラムでは、簡単のため、再描画イベントを受け取ったときにはウィンドウ全体を再描画する。 このため連続した再描画イベントはひとつにまとめて、全体で一回だけ再描画すればよい。 最初の while ループは、XCheckTypedEvent() を呼び出して、連続した再描画イベントをスキップするためのものである。
実際の描画は別な関数 Redraw() によっておこなわれる。
static void Redraw(Display* d, Window w, GC gc, bool button_pressed) { static char* message = "Click Here!"; if (button_pressed) XFillRectangle(d, w, gc, 10, 10, 200, 70); else XDrawRectangle(d, w, gc, 10, 10, 200, 70); XDrawString(d, w, gc, 20, 30, message, strlen(message)); XDrawLine(d, w, gc, 11, 82, 212, 82); XDrawLine(d, w, gc, 212, 82, 212, 11); }
この関数はウィンドウ中にボタンの絵を描く。 状態変数 button_pressed の値に応じてボタンの中を塗りつぶすか否かを変える。
ところで再描画をおこなうのは、再描画イベントを受け取ったときだけではない。 マウスのボタンが押し下げられて、ボタンを反転させなければいけなくなったときも再描画しなければならない。
case ButtonPress : button_pressed = OnButton(&event); // マウスがボタンの絵の上なら true ClearAndRedraw(display, win, gc, button_pressed); break;
このときの再描画は ClearAndRedraw() 関数を呼び出しておこなう。 再描画イベント発生時、再描画する部分は自動的に背景色で塗りつぶされているので、何もする必要はない。 しかしそうでない場合は、あらかじめ明示的にウィンドウを背景色で塗りつぶさ (クリアし) なければならない。
static void ClearAndRedraw(Display* d, Window w, GC gc, bool button_pressed) { XClearWindow(d, w); Redraw(d, w, gc, button_pressed); }
直接 Redraw() を呼ぶのではなく、ClearAndRedraw() を呼ばなければならないのは、マウスのボタンが離されたときも同じである。
case ButtonRelease : button_pressed = false; MouseClick(display, win, &event); ClearAndRedraw(display, win, gc, button_pressed); break;
マウスが離された際の処理は MouseClick() がおこなう。 この関数は、マウスの位置を調べ、ボタンの絵の内側であればプログラムを終了させ、そうでなければ return する。
static void MouseClick(Display* d, Window win, XEvent* e) { XButtonEvent* event = (XButtonEvent*)e; int x = event->x; int y = event->y; int button = event->button; if (button == Button1 && 10 < x && x < 210 && 10 < y && y < 80) { XCloseDisplay(d); exit(0); } }
なお Button1 は、マウスの左ボタンを表わす定数である。
プログラムをコンパイルするには
とする。Makefile を使ってもよい。
なおコンパイルには teacup.bitmap も必要である。
Copyright (C) 1999-2000 Shigeru Chiba
Email: chiba@is.tsukuba.ac.jp