バグが出ても直しやすく、また機能拡張しやすいプログラムを書くための要点を再び示すと、
・関係のある処理はできるだけ (プログラム・ファイル中の) 近い場所にまとめる。
・類似の処理を重複して記述しない。 類似の処理はできるだけひとつの関数などにまとめる。
となる。 ところがイベント駆動型のプログラムは、simple.c の例でもわかるように、何らかの形でオブジェクト指向を導入しないと、上の条件をみたすのが簡単ではない。 各 GUI 部品に関する記述は、イベントの種類ごとに別々な関数の中にわかれて記述され、また同じ種類の GUI 部品があると、同じような処理を部品の数だけ重複して記述してしまいがちになる。
オブジェクト指向でソフトウェアを作成するための第一歩は、何をオブジェクトで表現するかを決めることである。 関係のある処理はできるだけまとめる、という観点から考えると、GUI 部品をオブジェクトとするのがよさそうである。 そこで X Window のウィンドウもひとつのオブジェクトであらわすと、GUI 部品をあらわすオブジェクト (GuiObject) とウィンドウをあらわすオブジェクト (Xwindow) の関係は次の図のようになる。
Xwindow はいくつかの GuiObject を含んでいる、という意味である。 プログラムで書くと、
class Xwindow { GuiObject* components[N]; }
となる。(完全なクラス定義。)
イベント駆動型のプログラムの難しさは、ひとえにイベント・ループの複雑さにある。 1個の関数が、例えば全ての GUI 部品に関するマウス・ボタンの押し下げイベントを処理するのでは、プログラムの保守性・拡張性は著しく悪くなる。 そこで、これを整理して、個々のイベントを対象となる GUI 部品(をあらわすオブジェクト)に送って、そちらで処理させるようにすれば、プログラムの複雑さは大きく改善される。
Xwindow オブジェクトは、次のように変形したイベント・ループを実行する。
void Xwindow::mainloop() { for (;;) { 次のイベントを受けとる ; switch (イベントの種類) { case Expose : for 全ての GuiObject obj obj->redraw(); break; case ButtonPress : obj = マウス位置の下にある GUI 部品 ; obj->buttonDown(マウス位置); break; case ButtonRelease : : } } }
このイベント・ループは、イベントを受けとった後、イベント処理関数を直接呼ぶ変わりに、そのイベントを処理するべき GuiObject を探し、そのイベントのメソッドを呼び出す。 GuiObject には、イベントの種類に応じたメソッドが用意してあり、それぞれ対応したイベントの処理をおこなう。
イベントをどの GuiObject に渡すかは、クラス・ライブラリの設計の大きな要点である。 もっとも融通がきく設計は、再描画イベントの場合のように、すべての GuiObject にイベントを渡す設計である。 しかしこの設計では、GuiObject の数が多くなると、実行効率が悪くなる。 実行効率の点では、例えばマスス・ボタンの押し下げイベントの際は、マウスが位置する GuiObject にだけイベントを渡す設計の方がよい。
ところがマウスのボタンを離したときのイベントも、同様にマウスが位置する GuiObject にイベントを渡すようにすると、simple.c で実現したような動きをこのライブラリでは実現できなくなる。 なぜなら、マウス・ボタンを離したとき、マウスの位置がボタンの絵の外にある場合には、ボタンの絵を非反転の状態にもどさなければならないからである。 もしマウスの位置がボタンの絵の外にある場合に、イベントを受けとれなければ、このような動作は実行できない。
このため、例として用意したクラス・ライブラリでは、マウス・ボタンを離したときのイベントは、マウス・ボタンが押されたときにマウスが位置していた GuiObject に渡されるように設計してある。
class GuiObject { public: GuiObject(Xwindow* w, int _x0, int _y0, int _width, int _height); bool inside(int x, int y); virtual void redraw(); virtual void buttonDown(int x, int y); virtual void buttonUp(int x, int y); virtual void keyDown(int key); protected: Xwindow* win; int x0, y0, width, height; };
コンストラクタが受け取るのは、GUI 部品の位置 (_x0, _y0) 、幅 width 、高さ height である。 メソッド inside() は、mainloop() がマウスの場所に位置する GUI 部品を探すときに使われる。 与えられた座標 (x, y) が、コンストラクタに与えられた位置、幅、高さから決まる領域の内側にあれば true 、そうでなければ false を返す。
redraw(), buttonDown(), buttonUp(), keyDown() は、その GuiObject 宛てのイベントが発生したときに呼ばれるメソッドである。 buttonDown(), buttonUp() の引数は、イベント発生時のマウスの位置を表す。 keyDown() の引数は押された文字である。
GuiObject は GUI 部品を表すオブジェクトの抽象クラスであり、redraw(), buttonDown(), ... は何もしない空のメソッドである。 意味のある処理は、GUI 部品の種類にあわせて個別に定義される GuiObject のサブクラスで定義される。
例えばウィンドウ上に枠で囲まれた文字列を表示するだけの部品を表す Label の定義は次のようである。(実際の定義は label.h, label.cc.)
class Label : public GuiObject { public: Label(Xwindow* w, int _x0, int _y0, int _width, int _height, char* str) : GuiObject(w, _x0, _y0, _width, _height) { string = str; len = strlen(string); } void redraw() { win->drawRectangle(x0, y0, width, height); win->drawString(x0 + 10, y0 + 20, string, len); } private: char* string; int len; };
ボタンなどと違って、Label はマウスのクリックなどに対して反応しないので、redraw() 以外のメソッドは上書きされない。 redraw() は、呼ばれると、この GUI 部品をウィンドウ上に描画する。
main(int argc, char** argv) { Xwindow* w = new Xwindow(10, 10, 300, 200, argc, argv); w->add(new Label(w, 10, 10, 200, 70, "Up")); w->add(new Label(w, 10, 100, 200, 70, "Down")); w->mainloop(); }
まず Xwindow オブジェクトを生成し、次に Label オブジェクトを二つ生成し、それぞれを add() を使ってウィンドウ上に貼り付ける。 必要な GUI 部品を生成し、ウィンドウ上に貼り付け終わったら、mainloop() を呼んでイベント・ループを実行する。
以上のような構成にすると、一見、simple.c のようなオブジェクト指向技術を使わないプログラムに比べて保守性や拡張性の悪いプログラムになったように思うかもしれない。 しかしオブジェクト指向技術を使った上のプログラムでは、Label の数を増やすときも、変更するのは main 関数だけである。
また新しい種類の GUI 部品を作る際も、GuiObject のサブクラスを作り、その部品固有の処理だけを記述すればよい。 redraw() はその GUI 部品だけを描けばよく、他の部品を描くためのメソッドを修正する必要はない。 また buttonDown() は、その GUI 部品上でマウス・ボタンが押されたときの処理だけをおこなえればよく、他の部品の状態を考慮する必要はない。
ここまで、simple.c のようなオブジェクト指向技術を使わないで書いたプログラムを、オブジェクト指向技術を使って書きなおすという作業をおこなった。 今度は、こうして書いたプログラムを、他のプログラマから使われるクラス・ライブラリ、つまり GUI toolkit として見てみよう。
商用の X toolkit や MFC (Microsoft Foundation Class) などを使うときを考えてみればわかるように、このような GUI toolkit を利用するプログラマは、自ら新しい GUI 部品を定義することはまれで、もっぱら toolkit が提供する部品を組み合わせるだけである。
そのような立場から考えると、toolkit を利用するプログラマが書かなければいけないのは、
main(int argc, char** argv) { Xwindow* w = new Xwindow(10, 10, 300, 200, argc, argv); w->add(new Label(w, 10, 10, 200, 70, "Up")); w->add(new Label(w, 10, 100, 200, 70, "Down")); w->mainloop(); }
だけで、simple.c に比べて非常に簡単に GUI プログラムが書けるようになったことがわかる。(もちろん商用の toolkit は機能が高い分、約束事が多く、main 関数も上のようには簡単にならない。)
プログラマが書かなければならないのは、必要な部品を生成し、配置を決めることだけである。
というよい性質をもつようになる。
ボタンの状態が変化して再描画が必要になったときは Xwindow の clearAndRedraw() を呼べばよい。 またプログラム全体を終了させるときは、Xwindow の quit() を呼べばよい。
例として提供されたプログラムをコンパイルするには次のようにする。
とする。 guitest という実行可能ファイルが作られる。
課題をやるには、label.h と label.cc に GuiObject のサブクラス Button の定義を付け加えて、main.cc を修正すればよい。
Copyright (C) 1999-2000 Shigeru Chiba
Email: chiba@is.tsukuba.ac.jp