MSDN マガジン > Home > 発行物 > 2008 > June >  GUI ライブラリ : Windows フォーム アプリケーションの簡潔さをネイティブ アプリ...
GUI ライブラリ
Windows フォーム アプリケーションの簡潔さをネイティブ アプリケーションで実現する
John Torjo

この記事では、次の内容について説明します。
  • GUI プログラミングの問題
  • ウィンドウ オブジェクトの作成
  • イベントおよび通知の処理
  • フォームとコントロール
この記事では、次のテクノロジを使用しています。
Win32 API、C++
C++ の GUI プログラミングの問題は、低レベルのライブラリが多く、プログラマの負担が大きいということです。C に類似した構造体に依存するか、複雑さを残したままのラッパー クラスを使用します。イベントのプログラミングも単純とはいえず、基本となる WM_ メッセージに関する知識が必要です。
この記事では、クライアント プログラマが GUI アプリケーションを処理するための高レベル インターフェイスとして筆者が作成した C++ ライブラリ、eGUI++ について説明します。このライブラリは、WM_ メッセージの知識がなくても使えるようにしただけのものですが、それだけで複雑さが隠蔽され、イベント プログラミングがきわめて単純になります。C に類似した元の構造体を処理する必要はなく、常にクラスを処理します。概して、eGUI++ のクライアント コードは解読しやすく、記述も単純です。
eGUI++ は Windows® 専用の機能です。クロスプラットフォーム GUI アプリケーションは、サイズが小さいか、単純なテスト フレームワーク、プロトタイプ、または教育目的の場合以外にはあまり信頼性がありません。さらに言えば、基本 OS の機能を利用する方が信頼性が高いのは確実です。Windows XP や Windows Vista® には、たくさんの機能があります。

ネイティブで移植可能
CLR コードをお探しなら、既にマネージ C++ があります。マネージ C++ は優れたプラットフォームなので、改善する必要はありません。Windows 2000 以降のオペレーティング システムに対応するネイティブ Windows コードを生成するための優れたライブラリをお探しなら、このコラムを最後まで読んでください。これから、使い方が簡単で、ターゲット OS の機能を活用したライブラリを紹介します。Microsoft® .NET Framework も不要です。作成するコードは C++ に類似しています。また、作成するコードは Visual C++ コンパイラ固有ではありません。g++ (GNU C++ コンパイラ) 4.1 でもコンパイルできます。基本的に、Win32® API をラップする場合、移植可能なコードの作成を妨げる要因はありません。
とはいえ、複雑な GUI の場合、Visual Studio® 2005 または Visual Studio 2008 Express エディションなどの優れた IDE が必要になります。筆者は、自作のライブラリを Visual Studio 2005 Express 以降と統合し、GUI がより使いやすくなるように調整しました。特に注目したのは、新しい GUI クラスを作成したり、既存の GUI クラスを拡張する場合に、IDE を最大限に活用するコード補完機能です。
筆者は GUI アプリケーションの作成を楽しくしたいと考え、eGUI++ 作成の目標を、解読しやすく、作成が簡単な GUI コードを作成することに定めました。たとえば、可能な限り、コード補完機能を実装しました。GUI プログラミングは安全になり (エラーが発生した場合は可能な限りコンパイル時に捕捉し、捕捉できなかった場合はランタイム例外をスローします)、eGUI++ はリソース エディタとの組み合わせが容易です (リソース エディタの Visual Studio 2005 以降のバージョンと連携しています)。

windows.h がない
windows.h をインクルードする場合の主な問題は、エラーが発生しやすいということです。発生しないイベントの監視をやめる理由は何でしょうか。たとえば、ボタン クラスがキーボード イベントを待機しても、このイベントは発生しません。
そもそも windows.h が必要な理由があるでしょうか。この場合、通常の C++ クラスを使用して C++ で Windows アプリケーションを作成できるようにする必要があります。そうすれば windows.h の内部を意識する必要がありません。WM_LBUTTONDBLCLK や WM_LBUTTONUP などの長いイベント名や、LPNMITEMACTIVATE や NMHDR などのあやふやな C 構造体を知らなくても済みます。C のようなキャストも不要になります。優れた C++ GUI ライブラリの主な機能は、Win32 API を抽象化し、クラスで処理できるようにすることです。
図 1 のような元の C 構造体を処理するのはもうたくさんだと思いませんか。そう思った筆者は、次のようなコードを作成しました。
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i); 
// The old way 
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
  REBARCLASSNAME, NULL,
  WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
  WS_CLIPCHILDREN|RBS_VARHEIGHT|
  CCS_NODIVIDER,
  0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);  
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask  = RBBIM_COLORS | RBBIM_TEXT |
  RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
ご覧のように、eGUI++ は複雑な処理を隠蔽しています。開発者は C++ クラスのみを使用し、複雑な API 関数 (CreateWindowEx など)、定数名 (WS_* 定数)、複雑な C 構造体 (REBARPARAMINFO など) を頭に入れる必要がありません。
eGUI++ は Windows 2000 以降を対象にしています。既定では、Windows XP SP2 を対象にしていますが、より機能の少ない、またはより機能の多い別の OS を対象に選択することもできます。別の OS を対象にする場合は、eGUI++ ヘッダーをインクルードする前に、#define EGUI_OS で対象 OS の定数を指定します (図 2 を参照)。
// code from eGUI++
struct os {
 typedef enum type {
 win_2k,
 win_2k_sp4,
 win_xp,
 win_xp_sp2,
 win_vista
 };
};

#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
コード内の #ifdefs の数が少ないため、はるかに読みやすくなります。代わりに、OS 固有のプロパティが指定されているのがおわかりでしょう。
property<int,os::win_xp> some_prop;
古い OS をターゲットにして上記のプロパティを使用すると、コンパイル時エラーなどが発生します。

各ウィンドウの処理
通常、オブジェクトの生存期間を決めるのはプログラマです。ところが、GUI プログラミングの重大な問題は、ビジュアル ウィンドウとウィンドウ オブジェクトとの間に差異があり、ウィンドウを閉じるタイミングを決めるのはユーザーだということです。コード内では、ユーザーによって破棄されたウィンドウのウィンドウ オブジェクトへの有効なポインタまたは参照を持つことができます。そのため、オブジェクト インスタンスを表す画面上のウィンドウと画面上のウィンドウを表すオブジェクト インスタンスの 1 対 1 対応を維持することは困難です。このようなシナリオの 1 つの明確な制限は、スコープの終了時に破棄されるローカル ウィンドウ インスタンスを保持できないということです。
{
form f(...);
f.show();
...
}
画面にフォーム f を表示するとします。f のスコープが終了するとどうなるでしょうか。この場合の選択肢は、画面上のフォームを破棄するか、対応する C++ インスタンスを破棄して画面上のフォームをそのままにしておくかのどちらかですが、どちらの方法も適切ではありません。前者の場合、フォームが見えなくなったユーザーが混乱します。後者の場合、画面上にフォームは残っていますが、対応する C++ インスタンスが破棄されているため、イベントに応答しません。また、f がスコープ内にあるときにユーザーが画面上のフォームを既に閉じている可能性もあります。この場合、画面上に存在しないオブジェクトを操作することになります。
この問題の解決策は、常に (間接的な) ポインタを使用してウィンドウにアクセスすることです。ユーザーが画面上のウィンドウを閉じると、対応する C++ インスタンスに無効のマークが付けられ、そのウィンドウにアクセスしようとすると例外がスローされます。次のように、ウィンドウの有効/無効はいつでも確認可能であり、プログラマがウィンドウを破棄することもできます。
wnd<> w = ...;
// is window valid?
if ( is_valid(w) ) w->do_something();
// is window valid?
if ( w) w->do_something();

// destroy the window
delete_(w);
画面上の各ウィンドウに対応する C++ インスタンスが存在するため、wnd<> テンプレート クラスを使用してウィンドウを処理します。このテンプレート クラスはウィンドウへの共有 (参照カウント対象) ポインタを表します。wnd<> クラスにはウィンドウの種類を指定するオプション引数があります。これは基本的に予期されるとおりです。既定では window_base ですが、テキスト、ラベル、Rebar、エディットなどのウィンドウ クラスを指定することもできます。図 3 に、ウィンドウ オブジェクトの使い方とウィンドウ オブジェクト間のキャストの例を示します。
// when constructing a window, you can specify its type
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));

// when constructing a window, if you don't specify its type,
// it will guess it, based on who you assign it to
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;

// destroying a window
delete_(b);

// casting - if it fails, it throws
wnd<form> f = wnd_cast(w);

// casting - if it fails, returns null
if ( wnd<edit> e = try_wnd_cast(e) )
  e->text = "not nullio";
ウィンドウを作成する場合は new_ 関数を使用し、削除する場合は delete_ 関数を使用します。ここでも、ウィンドウを閉じるのは通常はユーザーです。また、X 型 (既定は window_base) のウィンドウが Y 型であるかどうかも確認するには、キャストを使用します。キャストは常に明示的です。キャストには、失敗時に例外をスローする wnd_cast と、失敗時に null ウィンドウを返す try_wnd_cast の 2 種類があります。
eGUI++ クラスの開発時には、基本クラスから派生すると共に、サイズ変更やスキン化などの追加動作も継承すると便利な場合があります。このような場合、複数の再利用可能な動作クラスを作成し、追加で派生することができます。

簡単なコード
作成および解読が容易な GUI コードを目にすると気分爽快になります。筆者は、あらゆる裏技を駆使して可能な限りコード補完機能を活用できるようにしました。大規模な GUI ライブラリを使用するときによく忘れがちなものにプロパティ名、イベント名、フラグなどがあります。サンプル コードではこれらにも対処しています。ドキュメントには Doxygen を使用しました。これは優れたシステムです。使えば使うほど愛着が湧きます。
ドキュメントの参照は容易です。プロパティ名の場合、図 4 のように「w->」だけを入力すれば、メソッド名とプロパティ名が表示されます。プロパティはメンバ変数として表示されるため、区別が容易です。イベント名の場合、クラスで処理可能なイベントを簡単に憶えられます。スコープ演算子の後に「クラス名::ev::」と入力するだけで、コード補完機能によって該当するイベントにジャンプして表示します。とはいえ、eGUI++ の真価が発揮されるのは、フラグの処理です。フラグの組み合わせになり得る各プロパティについて選択可能なフラグ オプションを検索する場合は、フラグ プロパティに "." を付加すれば、コード補完機能が補助してくれます。おまけとして、プロパティに演算子オーバーロードを追加しました。その結果、次のコードは有効です。
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
図 4 コード補完機能
また、プログラマが使用できるコントロール一覧の内容を忘れたときのために、egui::ctrl という名前の名前空間を追加しました。

コントロールとフォーム
Win32 GUI プログラミングの経験があるプログラマにとっては、ダイアログはお馴染みです。API でのダイアログの作成 (::CreateDialog) は、ウィンドウの作成 (::CreateWindow[Ex]) とは方法が異なることもご存知でしょう。プログラマは、シグネチャがまったく異なる 2 つの複雑な関数の使い方を覚える必要はありません。eGUI++ ではどちらもウィンドウ型として扱い、ウィンドウの作成方法は 1 つです (new_ 関数を使用)。
種類の異なるウィンドウを扱う場合は、名前ダイアログよりも名前フォームを使用した方が表現力が高まります。名前フォームは他のコントロールを表示するウィンドウを記述したものであり、表示される各コントロールがデータを保持しています。ダイアログとフォームのどちらを使用してもかまいませんが、筆者はフォームの方が好みです。実際に表示されるコードは次のとおりです。
typedef form dialog;
概念的には、ウィンドウの種類はコントロールとフォームの 2 種類のみです。コントロールは、ユーザーに変更を許可することが可能なデータを表示するウィンドウです。すべてのコントロール クラスは control クラスから派生します。フォームは、1 つ以上のコントロールとその固有のロジックの一部 (一部のデータの操作を許可するロジックなど) をホストするウィンドウです。
各ウィンドウ型で使用可能な機能は、ウィンドウの動作によって異なります。たとえば、フォームでは子コントロールを列挙できますが、コントロールでは子コントロールを列挙できません。このように、コード エラーが発生しにくくなっています。また、通常はリソース エディタを使用してフォームにコントロールを配置済みなため、コントロールを作成する必要はほとんどありません。
フォームにもモーダル ダイアログとメッセージ ボックスの 2 種類があります。モーダル ダイアログを作成するには、フォームの作成時に form::style::modal を追加するだけです。メッセージ ボックスを作成するには、次のように msg_box<> 関数を使用し、ボタンをテンプレート引数として指定します。
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
    std::cout << "ok pressed";
また、msg_box<> はボタンの組み合わせが有効かどうかをコンパイル時に判断します。
// ok
   msg_box<mb::yes | mb::no>("q");
   // compile-time error
   msg_box<mb::ok | mb::yes>("q");

フォームのプログラミング
繰り返しになりますが、フォームのことを Win32 API ではダイアログと呼びます。Windows フォームの場合、フォームのプログラミングが優れていることは実証済みです。各フォームになんらかのコントロールがあり、フォームごとに 1 つのタスクを解決します。旧来の複雑なシングル ドキュメント インターフェイス (SDI) やマルチ ドキュメント インターフェイス (MDI) の代わりに、タブを使用してコントロールや他のフォームをホストできます。そのため、CFrameWnd や CMDIChildWnd に類するものを目にすることはありません。必要ないからです。1 つのフォーム上で複数のフォームをホストする必要がある場合は tab_form クラスを使用します。このクラスでは、それぞれ固有のタブ上に子フォームを追加できます。

フォームの処理
ウィザードは好みではないのですが、いくつかのウィザードを使ってプログラミング作業が楽になることも確かにときどきあります。そこで、フォームを作成するための New Class Wizard を作成しました。クラス ビューで [Add Class] を選択し、[Categories] で [eGUI] を選択します。左側で eGUI フォームを選択し、[Add] をクリックして、クラス名を指定します。それで終わりです (図 5)。ウィザードは、eGUI++ の内部で保持するヘッダー ファイル <ダイアログ名>.h、ソース ファイル <ダイアログ名>.cpp、追加ヘッダー ファイル <ダイアログ名>_form_resource.h を作成します。
図 5 Add Class (画像をクリックすると拡大表示されます)
最後のヘッダー ファイルにはフォームで使用するすべてのコントロールの名前が格納されます。そのため、MFC のようにデータのやり取りに使用するコントロール変数を追加作成する必要がありません。コントロールを直接使用します。2 つのエディット ボックス ([User Name] (ユーザー名) と [Password] (パスワード)) と 2 つのボタン ([OK] と [Cancel] (キャンセル)) を持つ図 6 のようなログイン ダイアログがあるとしましょう。
図 6 エディット ボックスとボタン
次のファイルが自動生成されます。
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form, 
 private form_resource::login {};

// login.cpp
#include "stdafx.h"
#include "login.h"
コードはごく単純です。"enum {IDD = ... }" のようなウィザード スタイルのコードやメッセージ マップはありません。不要であれば、カスタム コンストラクタを指定する必要もありません。既定の設定を使用すれば十分です。
ログイン クラスは form_resource::login からプライベートに派生し、login_form_resource.h (このファイルは eGUI++ ライブラリで保持されます) に実装されます。form_resource::login クラスにはフォームのコントロールに関する情報 (コントロールの名前、種類、コントロールからの通知を捕捉できるかどうか) が格納されます。クラスのメンバ データは通常プライベートですが、フォームでも同様で、フォーム上のコントロールは通常プライベートにする必要があります。
今度は、次のような form_resource::login が生成されます。
// login_form_resource.h
#pragma once
struct form_resource::login {
 // ... (code to allow 
 // handling of notifications)
 wnd<edit> username;
 wnd<edit> passw;
 wnd<button> ok, cancel;
};
これでフォームのコントロールが操作しやすくなります。パスワードが "secretword" であることを確認するとします。
void login::on_button_click(ev::button_click &, ok_) {
 if ( passw->text == "secretword")
 { pass_ok = true; visible = false; }
}
ご覧のように、Visual Basic® の場合と同様、フォームを非表示にするには、visible プロパティを false に設定するだけです。

古い ID が不要
リソース エディタを使ったことがある読者は、ID_、IDD_、IDC_、IDR_、IDS_ など多種のリソース プレフィックスを目にしたことがあるでしょう。プレフィックスはリソース エディタには有効です。ただし、コード内では、記憶する必要も意識する必要もない余分な情報です。eGUI++ アプリケーションではこれらのプレフィックスは完全に無視されるため、まったく意識する必要がありません。
たとえば、前述の名前 (username、passw、ok、cancel) はリソース エディタからのショートカットです。eGUI++ ライブラリはこれらの ID* のプレフィックスを自動的に削除します。元の名前は IDC_username、IDC_passw、IDOK、IDCANCEL だった可能性があります。

イベントおよび通知
前述したように、WM_ メッセージを記憶する必要はありません。とはいえ、イベントを使いこなすのは容易ではありません。イベントは数が多いため、応答可能なイベントを探し出してすぐに応答できるようにする簡単な方法が必要です。フォームのコントロール上で発生したイベントの通知を受けて、コントロールを拡張したり、固有のイベントを追加したりできるようにするための簡単な方法も必要です。
すべてのウィンドウ クラス (コントロールまたはフォーム) はイベントを生成できます。各ウィンドウ クラスにすべてのイベントを捕捉するイベント ハンドラがあります。イベントごとにイベントを処理する関数が定義されます。この関数は仮想関数であり、実装しても何も実行しません。各イベント ハンドラ関数には 1 つの引数 (イベント データ) があります。
既存のコントロールでは、対応するイベント クラスの名前は handle_events::control_name です。既存の各 eGUI++ ウィンドウ クラスの wnd_name は既に handle_events::wnd_name から派生しています。既存のウィンドウ クラスを拡張する場合、常にそのイベントを処理できます (わかりやすくするため、すべてのイベント ハンドラ関数を "on_" で始めます)。たとえば、次のようになります。
struct my_btn : button {
 void on_char(ev::char& e) {
 cout << "typed " << e.ch;
 }
};
他の GUI ライブラリを使用した経験があれば、一見簡単そうでも実はそうでないことをご存知でしょう。既存のコントロールは、イベントではなく通知を送信します。通知は WM_COMMAND/WM_NOTIFY メッセージの形式で送信され、送信先はコントロールではなく、コントロールの親です。一見、これは合理的なように思われます。通知を必要としているのはコントロールの親 (フォーム) です。ところが、これではコントロール クラスの拡張はきわめて困難です。現在のファイル システムを視覚化するツリーが必要な場合はどうでしょう。この場合、アイテムの展開 (TVN_ITEMEXPANDING) などのイベントを捕捉する必要がありますが、これらのイベントはコントロールの親に送信されます。したがって、プログラマが通知をコントロールに渡す手段が必要です。
eGUI++ では、通知はイベントです。通知は常にコントロールに送信され、その後でコントロールの親に送信されます。コントロール クラスを継承によって拡張すると、各通知はそれぞれ異なるイベントに変換されます。たとえば、ユーザーが最初の列を編集しているときに、エディット コントロールではなくコンボボックスを表示するリスト コントロールを作成する場合のコードは次のようになります。
struct list_with_combo : list {
 ...
 void on_begin_label_edit(
  ev::begin_label_edit & e) {
 e.allow_default = false;
 combo->rect(...);
 combo->visible = true;
 }
 wnd<combo_box> combo;
};
イベントを処理するには、次のようにイベント ハンドラ関数をオーバーロードします。
struct my_btn : button {
 void on_char(ev::char& e);
};
ここでは、文字キーが押されたときのイベント (Win32 API を使用する場合は WM_CHAR メッセージ) に応答します。
on_my_event イベント ハンドラ関数のイベント引数は常に ev::my_event 型であることを思い出してください。クラスが処理できるイベントはすべて ev:: 構造体です。「ev::」と入力するだけで、コード補完機能により、クラスで処理可能なすべてのイベントが表示されます (図 7 を参照)。イベント情報を探す最も簡単な方法は、「e.」と入力し、コード補完機能でイベントに関連するすべてのデータを表示することです (図 8)。
図 7 コード補完機能によるイベントの表示
図 8 イベント情報の取得
コントロールのイベントに関するドキュメントを参照する場合は、コントロールを選択してから ev クラスを選択すると、すべてのイベントが表示されます。ライブラリでは、同じイベントを複数のイベント ハンドラに送信できます。たとえば、通知はコントロールに送信された後、コントロールの親にも送信されます。
すべてのイベントに .sender プロパティがあります。これはイベントを送信したコントロールであり、通知の処理、特に通知の送信元を確認する場合に役立ちます。すべてのイベントに .handled プロパティがあります。このプロパティに設定可能な値は handled_partially (既定) と handled_fully の 2 つです。このプロパティを handled_fully に設定することにより、イベントの処理を停止できます。他のイベント ハンドラが残っていても、それらのハンドラは呼び出されません。たとえば、edit クラスを拡張する際にテキストの変更を親に通知しないようにするには、次のようなコードを作成します。
struct independent_edit : edit {
                      void on_change(ev::change &e) {
                      e.handled = handled_fully;
                      }
                     };
上記のように、コントロールの拡張は単純です。フォーム上の通知の処理もこのように単純にする必要があります。通知を処理する場合、送信元 (e.sender) を知っている必要があります。それ以上に、特定のコントロールからの通知を操作する機能が欲しいところです。そこで、イベント ハンドラ関数に、コントロール名の後にアンダースコア (_) を付加した引数を追加します。たとえば、ユーザーが username エディット ボックスに入力した内容を確認するには、次のようなコードを実行します。
void login::on_change(
 edit::ev::change &e, username_) {
 cout << "name=" << e.sender->text;
}
たとえば、米国ドルとユーロとの間で通貨を換算するとします。[EUR] ボックスに値を入力し、何か入力すると、[USD] ボックスの値が更新されます。また、[USD] ボックスに値を入力し、何か入力すると、[EUR] ボックスの値が更新されます (図 9 を参照)。この処理を行うコードは次のようになります。
struct convert : form, form_resource::convert {
 double rate; 
 convert() : rate(1.5) {}
 int mul_str(const string& a, double b) { ... }
 void on_change(edit::ev::change&, eur_) {
 usd->text = mul_str ( eur->text, rate); }
 void on_change(edit::ev::change&, usd_) {
 eur->text = mul_str ( usd->text, 1/rate); }
};
図 9 通貨コンバータ
コードの内容は一目瞭然です。mul_str は、string を double に変換して double 型と string 型の乗算を行い、その値と換算レートを乗算します。
前述したとおりにイベントを処理するためには相当の作業が必要です。フォーム上に 3 つのエディット ボックスがあるとします。各エディット ボックスは一連のイベントを生成します。次のように、on_change などの各イベントに対してコントロールごとに 1 つずつオーバーライド可能な関数を生成することができます。
void on_change(edit::ev::change& e, ctrlname_);
または、次のようなオーバーライド関数を 1 つ生成する方法もあります。
void on_change(edit::ev::change& e);
筆者なら前者の方法を選びます。こちらの方がクライアント コードがはるかに単純で、Visual Basic の方法により類似しています。処理内容も簡単にわかります (後者の方法では、イベントの実装でプログラマが e.sender を使用して、イベントを生成したコントロールを手動で問い合わせる必要があります)。
そこで最初の方法を実装することにしました。ただし、この処理の裏には eGUI++ によるリソース エディタの監視という多大な作業が伴っています。新しいコントロールを追加したり、コントロールの名前を変更したりすると、すべての <ダイアログ名>_form_resource.h ファイルが更新されます。form_resource::<ダイアログ名> クラスの各 <ダイアログ名>_form_resource.h ファイルについて、既存のコントロールからのすべての通知を上書きし、上書きされた各通知を送信できるコントロールを探す必要があります。次に、各コントロールについて、上書き可能な別の関数に転送する実装を生成します。例として、2 つのエディット ボックスと 2 つのボタンを持つログイン フォームのコードを図 10 に示します。
struct form_resource::login {
  wnd<edit> name;
  wnd<edit> passw;
  wnd<button> ok, cancel;

  typedef ... ok_;
  typedef ... cancel_;
  typedef ... name_;
  typedef ... passw_;

  virtual void on_change(edit::ev::change& e, name__) {}
  virtual void on_change(edit::ev::change& e, passw__) {}

  virtual void on_change(edit::ev::change& e) {
    if ( e.sender == name) on_change(e, name__());
    else if ( e.sender == passw) on_change(e, passw__());
  }
  // ... same for other edit notifications

  virtual void on_click(button::ev::click & e, ok__) {}
  virtual void on_click(button::ev::click & e, cancel__) {}
  virtual void on_click(button::ev::click & e) {
    if ( e.sender == ok) on_click(e, ok__());
    else if ( e.sender == cancel) on_click(e, cancel__() );
  }
  // ... same for other button notifications
};
new_event<> からの派生によって固有のイベントを作成することもできます。既存のイベントを送信する場合も、自作の固有イベントを送信する場合も、手順は同じです。次のように、send_event 関数を使用します。
struct hover : new_event<hover> {
 int x,y; // position
 hover(int x,int y) : x(x),y(y) {}
};

w->send_event( hover(x,y) );
ライブラリはスレッド セーフです。参考までに付記しておきますが、筆者は、各ウィンドウが持つ m_cs mutex 変数 (これは基本的に CRITICAL_SECTION です) を使用して各メソッドのアクセスがスレッド セーフであるかどうかを確認しています。ウィンドウ クラスを拡張する際は、m_cs 変数を再利用しても、固有の変数を作成してもかまいません。

メニューやショートカットなど
GUI プログラミングの経験があれば、メニュー コマンドを押した場合にも、キーを押した場合にも WM_COMMAND が送信されることをご存知でしょう。そのため、WM_COMMAND を受け取った場合にイベントの送信元がコントロールであるか、メニューであるか、(またはこの場合はキーボード ショートカットであるか) を判別することが困難です。eGUI++ では、メニューをフォーム (ダイアログ) 上に直接配置することでこの問題の一部を解決できます。フォームに送信されたコマンドの送信元がボタンでなければ、送信元はメニューです。
これでメニュー コマンドを処理できます。次はショートカットを見てみましょう。ショートカットの問題は、キー入力はいつでも (たとえば、エディット ボックス内でも) 可能だという点にあります。キーボード ショートカット (アクセラレータ) は最初に直接ウィンドウに送信され、次にウィンドウをホストするフォーム、フォームの親という順に、最上位ウィンドウに達するまで階層の上位に向かってルーティングされます。ショートカットのイベント ハンドラが最初に見つかったところで処理は停止します (特定のショートカットは複数のウィンドウでは処理されません)。
これでツール バーが残ります。ツール バーはメニューおよびショートカットと連携しています。ツール バー ボタンを押すと、イベントはメニュー コマンドに変換され、コマンドの送信元がメニュー、ショートカット、ツール バー ボタンのいずれであるかに関係なく、そのイベントをホストするフォームに直接ルーティングされます。
次のように、メニュー コマンドを処理するフォームを実装するとします。
void on_menu_command( ev::menu&, 
 menu::some_menu_id) { ... }
2 つのメニュー コマンド (new_file および open_file) を処理するには、次のようなハンドラを作成します。
void on_menu_command( ev::menu&,
    menu::new_file) { ... }
   void on_menu_command( ev::menu&,
    menu::open_file) { ... }

タブ コントロールとフォーム
タブはよく使われる GUI パラダイムです。タブ コントロールを拡張して、tab_type プロパティに normal または one_dialog_per_tab (この場合、タブ コントロールが他のフォームをホストします) の値を指定できるようにします。後者の場合、次のようにして新しいフォームを追加できます。
tab->add_form<form_type>( new_([args]) );
前述のようなログイン フォームを作成するには、次のようなコードを記述します。
tab->add_form<login>( new_() );
1 つ以上のフォームを追加したら、このタブ フォームに保持可能なタブの数を指定できます。
tab->count = 5;
ここでは、タブの数を 5 つにします。このためには、最後に追加したフォームを必要な数だけ複製します。以前にタブが 1 つしか存在しなかった場合、最初のタブ上のフォームがあと 4 回複製されます。そうです、既存のどのウィンドウでも複製できます。
ここまでは、イベントの侵入型の処理を示してきました。つまり、ウィンドウ クラスを拡張し、最終的にイベントに応答します (または、フォームを実装している場合は通知に応答します)。ところが、場合によっては、相互に関連のない複数のウィンドウに適用する動作を実装する必要があります。
サイズ変更とスキン化を例にとってみましょう。これらの機能は非侵入型の方法 (動作を実装するクラスを作成し、そのクラスから GUI クラスを派生する方法) で実装できます。ただし、この方法ではコードが複雑になり、スキン化の場合などは常に実現可能であるとは限りません。動作を非侵入型の方法で実装すれば、その動作を他のアプリケーションで再利用できるうえ、動作を無効にすることも簡単です。
たとえば、非侵入型のイベント ハンドラ クラスを作成し、このクラスのインスタンスを作成して、登録します。新しいウィンドウを作成すると、ハンドラ インスタンスが通知され、このインスタンスを監視することができるようになります。この場合、次のように、監視の対象にするイベントを手動で指定する必要があります。
// monitor button clicks
struct btn_handler : non_intrusive_handler {
 void on_new_window_create(wnd<> w) {
 if ( wnd<button> b = try_cast(w)) {
  b->events.on_click += mem_fn(&on_click,this);
 }
 }
 void on_click(button::ev::click&) { ... }
};
イベント ハンドラの登録は簡単です。次のコードを実行するだけです。
btn_handler bh;
window_base::add_non_intrusive_handler(bh);

サイズ変更機能を実装する場合は、アプリケーションに応じてさまざまな方法があります。たとえば、新しいフォームのサイズに基づいて各フォームの on_size イベントを上書きし、コントロールの位置を更新することも可能です。ただし、これは作業量が多く、良い方法とは言えません。または、各フォーム上に "a.x = b.x + b.width + 4;" のようなコントロール間の関係を作成することもできます。柔軟性の高い方法ですが、この場合も作業量は多くなります。
あるいは、各フォーム上の各座標に関して、コントロールにサイズ変更可能または移動可能のマークを付ける方法もあります。コントロールに特定の座標上でのサイズ変更可能のマークを付けると、フォームのサイズを変更したときにそのコントロールのサイズが更新されます。コントロールに特定の座標上での移動可能のマークを付けると、フォームのサイズを変更したときにそのコントロールが移動します。多くのアプリケーションにはこれで十分です。この発想は WTL の CResizeWindow から拝借したもので、非侵入型のハンドラを使用して実装しました。図 11 のようなダイアログを想定してみましょう。このダイアログのサイズを変更すると、図 12 のようになります。この処理のコードは次のようになります。
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
図 11 ダイアログ
図 12 サイズ変更後のダイアログ
GUI の操作が失敗すると必ず例外がトリガされます。このしくみにより、問題が発生したことがわかります。デバッグ モードではアサーション エラーが生成され、プログラムはデバッグ モードに入ります。これは、エラーを警告なしで無視するよりはるかに好ましい動作です。問題が発生していることが視覚的にわかり、バグを探すことができます。

Visual Studio 2005 との統合
Visual Studio は優れた IDE であり、その主なメリットは拡張可能であるという点にあります。eGUI++ はその Visual Studio を利用しています。Visual Studio は New Form Class Wizard を提供するアドインに付属しています。また、Visual Studio には Visual Basic に似たバーがあり、それを使ってフォーム上のコントロールの通知を処理できます。これを行うには、コントロールを選択します。すると、そのコントロールで生成可能な通知の一覧が表示されます。処理済みの通知は太字で表示されます。イベントをクリックすると、まだ一覧に存在しない場合はハンドラが追加されます。たとえば、図 13 のようになります。
図 13 フォーム上のコントロールの通知の処理 (クリックすると拡大画像が表示されます)
内部では、eGUI++ はリソース エディタを監視し、変更が発生するたびに、必要に応じて _form_resource.h ファイルを更新します。これまでに詳しく説明したように、コード補完機能も使用できます。

動作の実装
GUI を構築したら、次に、動作を実装してデータをバインドできるようにします。多くのフォームはデータ収集専用に使われます。初歩的な方法としては、汎用フォーム クラスを実装し、操作するデータを構築時に取得して、フォームのコントロールにバインドします。これで、データを検証するための一連のルールを指定できるようになります。破棄時に、検証ルールが成功した場合、元のデータはコントロールからの値に更新されます。検証ルールが失敗した場合、元のデータは不変です。このように、各新規フォームについて必要な作業は、リソース エディタでフォームを作成し、データの検証に使用する一連のルールを指定することだけです (新規フォーム クラスを作成してロジックを複製する必要はありません)。
今後のことを考えて、標準テンプレート ライブラリ (STL) 配列とコレクションとの差異や、リスト コントロールとツリー コントロールとの差異を埋めることができます。たとえば、employees 配列とリスト コントロールがあるとします。employees 配列とコントロールをバインドするには、次のようにします。
list_ctrl->bind(employees);
この場合、予想されるとおり、リスト コントロールが更新されます。リスト コントロールのセルを変更すると、変更が employees 配列と自動的に同期されます。
eGUI++ の作成目的は、GUI プログラミングが楽しくなるような優れたライブラリを作成することでした。C++ プログラマの読者にはこの目的に共感していただけるものと期待しています。ソースおよびバイナリは torjo.com からダウンロードできます。

John Torjo は、10 年以上 C++ でのプログラミングを経験した今でもこの言語を愛好する C++ プログラマです。ログ記録と GUI を得意とし、難題を課されることを楽しんでいます。難しい課題があれば、彼に電子メールを送ってください。詳細については、John のサイトである torjo.com を参照してください。

Page view tracker