もっとも単純な方法は、ユーザに Button クラスのサブクラスを、動作ごとに個別に定義させる方法である。
class Button : public GuiObject { public: : virtual void mouseClick(); // 新たに追加 : }; void Button::buttonUp(int x, int y) { if (マウス位置がボタンの内側にある) mouseClick(); button_pressed = false; win->clearAndRedraw(); } void Button::mouseClick() { // 標準は何もしない。 }
Button クラスの定義をこのように修正すれば、ユーザは mouseClick() を上書きして、自分の好きな処理をさせられる。
class MyButton : public Button { public: void mouseClick() { puts("mouse click!"); } };
しかしこの方法には、ふたつの重大な欠点がある。
この問題を回避するために使われる技法がコールバック関数 (callback function) と呼ばれるものである。 簡単にいうと、ボタンが押されたときの処理は独立した関数として定義し、オブジェクトにはその関数のポインタをあらかじめ渡しておく。 そしてボタンが押されたときは、その関数ポインタがさす関数を実行する、というものである。
void Button::setCallback(void (*f)(GuiObject*)) { callback_function = f; } void Button::buttonUp(int x, int y) { if (マウス位置がボタンの内側にある) (*callback_function)(this); button_pressed = false; win->clearAndRedraw(); }
ここで関数ポインタ callback_function は Button クラスのデータ・メンバーである。
オブジェクト指向言語では、コールバック関数の代わりに、コールバックを受けるオブジェクトを定義してこれにかえる。 Java 言語の AWT では、このようなオブジェクトを Listener と呼んでいる。
Listener オブジェクトを使うと、Button クラスは次のように変更される。
class Listener { public: virtual void action(GuiObject*) { /* empty */ } }; : class Button : public GuiObject { protected: Listener* listener; /* Button クラスにメンバーを追加 */ : public: void addLister(Listener* obj) { listener = obj; } }; : void Button::buttonUp(int x, int y) { if (マウス位置がボタンの内側にある) lister->action(this); button_pressed = false; win->clearAndRedraw(); }
ここで listener は、Button クラスのデータ・メンバーで、型は Listener* である。
ボタンが押されたときの動作は、Listener オブジェクトのメソッド action() として定義される。 action() の中身は、動作の種類ごとに変えなければならないので、ユーザは Listener クラスのサブクラスを定義し、望みの動作を action() として定義しなければならない。 Button オブジェクトに、コールバックの受け手のオブジェクトとして登録するのは、この Listener のサブクラスのオブジェクトである。
class MyListener : public Listener { void action(GuiObject* obj) { puts("mouse click!"); } }; main() { : button->addLister(new MyListener()); : }
Listener オブジェクトの action() の中身は、動作の種類ごとに変えなければならないので、ボタンが押されたときの動作に合わせて多数の Listener のサブクラスを定義しなければならないことに違いはない。
しかし多重継承あるいは Java 言語の interface を活用すれば、わざわざ Listener のサブクラスを新たに定義しなくてもすむことがある。
class MyApplication : public Application, public Listener { public: void main(int argc, char** argv); void action(GuiObject* obj) { puts("mouse click!"); } }; void MyApplication::main(int argc, char** argv) { Xwindow* win = new Xwindow(...); : button->addListener(this); // Listener オブジェクトの登録 win->mainloop(); } main(int argc, char** argv) { MyApplication app; app.main(argc, argv); }
上のプログラムでは、既に存在する MyApplication オブジェクトに Listener オブジェクトとしての役割も負わせている。 これによって、Listener オブジェクトとして新たにオブジェクトを生成せずにすむようになる。 action() を Listener の独立したサブクラスの中で定義するのではなく、他の適当なクラス (例では MyApplication) の定義の中に、混ぜて定義するのがポイントである。 action() を定義したクラスが Listener を継承 (Java 言語では implements) していれば、そのクラスが他のクラスを継承していても、そのクラスのオブジェクトを Listener オブジェクトとして addListener() の引数に指定することができる。
クラスの継承機構を使った方法と異なり、複数の Button オブジェクトの動作を単一の Listener オブジェクトで制御することも可能である。 またその場合、Button オブジェクトがいくつかのサブクラスに分かれていても、定義しなければならない Listener のサブクラスはひとつである。
オブジェクト指向による設計では、機能ごとにオブジェクトを分割していくことが、ときに大切である。 分割することにより、プログラムの保守性や拡張性が改善されることが多い。 また Listener の例のように、分割することによって、プログラム中にあらわれるクラスの総数をおさえることもできる。 しかしながら、無闇にオブジェクトを細分化すればよいわけではない。 その細分化によって、本当に保守性や拡張性が向上しているか、確かめながら設計をすすめることが大切である。
1回目の初めに示したように引き算をおこなうプログラムを作れ。 (メニューバーは不要。)
subtract.cc, textbox.h, textbox.cc を元に拡張して作成せよ。 これらのプログラムをコンパイルするには、
とする。
textbox.h, textbox.cc 中で定義されているクラス Textbox は、文字列を入力するためのフィールドを表示する GUI 部品のためのクラスである。 当然、Textbox は GuiObject のサブクラスである。
Textbox が複数個存在するときには、最後にマウスでクリックした方の Textbox がキー入力を受けつける。(キーボード・フォーカスがあたっているという。) Textbox の buttonDown() は上書きされており、マウスのボタンが押されると、Xwindow の setKeyboardFocus() を呼び、キー入力イベントが以後、その Textbox に渡されるように指示する。
キー入力があるたびに、計算結果を更新して表示するため、Textbox の keyDown() を修正して、Lister オブジェクトを呼ぶようにせよ。 そして subtract.cc 側で Lister オブジェクトを定義し、計算結果が更新されるようにせよ。
クラス Textbox には、イベントの処理用メソッドの他に次のようなメソッドが用意されている。
これらのメソッドを使って、キー入力時に、引く数と引かれる数の値をとりだし、答のフィールドに値を書きこむようにすればよい。 (答のフィールドとして Label でなく Textbox を使う場合。 Label を使ってもよい。)
Copyright (C) 1999-2000 Shigeru Chiba
Email: chiba@is.tsukuba.ac.jp