<戻る

WinMain関数からメインウィンドウハンドルを手に入れるまでのおさらい


 WindowsアプリケーションはWinMain関数から始まって、その関数を抜けると終了します。相当昔の事ですが、Win32 Applicationで初めてプログラムをした時、WinMain関数にsprintf関数やcout等を用いても標準出力が出ずに何だコリャと困惑していました。Win32 Applicationには標準出力すら存在しないのです。

 Win32アプリケーションでは、ウィンドウが作成できないと話になりません。今でこそこれに関する書籍は山ほどありますから、それほど難しいハードルではなくなりましたが、それでも面倒な作業ではあります。

 ここではWinMain関数からメインのウィンドウを表示するまでの一連の作業をまとめました。また、クラス化するに当たって問題となる「WinProcプロシージャ関数の壁」についても記載してあります。


@ ウィンドウクラスの登録

 「ウィンドウクラス」というのはオブジェクト指向に出てくる「クラス」とは違います。Windowsが認識するウィンドウの「雛形」の事です。Windowsのアプリケーションは、OSが定めた統一した方法で動いており、ウィンドウもその方法に従って機能します。OSが定めるものは何か?それは登録時に必要なWNDCLASSEX構造体を見れば分かります。

struct WNDCLASSEX{
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
};


cbSizeはこの構造体の大きさです。
styleはウィンドウの基本性質を設定します。例えばCS_HREDRAWを設定すると、ウィンドウの幅を変えたりした時にウィンドウ全体を再描画するようにします。CS_DBLCLKSを設定するとダブルクリックに対するメッセージを出力するようになります。他にも色々ありますが、基本的にはCS_HREDRAW | CS_VREDRAWを設定します。
lpfnWndProcはメッセージを受け取るWndProc関数へのポインタを渡します。WndProc関数は、ウィンドウに1つ存在するグローバルなコールバック関数で、メッセージキューから送出されたメッセージの唯一の受け口です。わかりやすい機構なのですが、この関数がグローバルであるおかげで面倒になっているのです。
cbClsExtraに0以上の値を入れると、登録時に渡されるWNDCLASSEX構造体の後ろにメモリが動的に確保されます。これは、アプリケーションに特別な値を保持しておきたい場合に使用します。例えば、アプリケーション共通のポインタなどがある場合などに利用できます。この特別領域へのアクセスはGetClassLong関数とSetClassLong関数で行えます。
cbWndExtracbClsExtraと似ているのですが、こちらは登録されたウィンドウのインスタンス(ウィンドウオブジェクト)が生成されるたびに確保される特別領域です。この領域へのアクセスはGetWindowLong関数とSetWindowLong関数を用います。
hInstanceはアプリケーションのインスタンスハンドルです。
hIconはアイコンハンドルで、このアイコンがデスクトップ等に描画されます。
hCursorはカーソルハンドルで、アプリケーションは指定されたカーソルを使うようになります。
hbrBackgroundにはアプリケーションが画面を再描画する時に塗る色(ブラシ)を持つブラシハンドルを設定します。
lpszMenuNameはウィンドウが持つデフォルトのメニューのリソース名を指定します。NULLの場合、ウィンドウはメニューを持ちません。
lpszClassNameはこのウィンドウクラスの名前を指定します。適当な名前で結構です。
hIconSmはタスクバーなどに表示される小さいアイコンの情報を持つアイコンハンドルを指定します。


 以上から、OSが必要としているのは、
 ・ウィンドウの型
 ・メッセージの受け口
 ・アイコン
 ・カーソル
 ・背景色
 ・メニュー(あれば)
 ・設定の名前

であることが分かります。この内メッセージの受け口であるWinProc関数以外は「ウィンドウの基本機能」や「見た目」です。

 WNDCLASSEXの登録はRegisterClassEx関数で行います。

if(!RegisterClassEx(&WndClass))
   return 0;

登録に失敗すると、関数はゼロを返します。無事に登録できたら、次はメインウィンドウの生成になります。


A メインウィンドウの生成

 メインのウィンドウを作成する時にはCreateWindowEx関数を用います。

HWND CreateWindowEx(
DWORD dwExStyle,
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPCOID lpParam
);

dwExStyledwStyleには作成するウィンドウの見た目の細かなスタイルを定義できます。これは非常に沢山ありますのでMSDNを参照していただきたいと思います。
lpClassNameはウィンドウクラスで登録した名前を入れます。指定の名前の雛形がこのウィンドウに設定されます。よって、「ウィンドウクラスは複数登録してもかまわない」わけです。何となくウィンドウクラスは1つしかないのかなと思ってしまいますが、そう決まっているわけではありません。ウィンドウクラスにはWinProc関数が登録されています。これは「アプリケーションは複数のWinProc関数を持てる」という事になります。しかし、WinProc関数はグローバル関数です
lpWindowNameは作成するウィンドウに付けられる名前です。この名前がデフォルトでキャプションに表示されます。
x,yはウィンドウの左上の位置です。CW_USEDEFAULTというマクロ定数が設定できます。この場合、左上の位置はOSが適当に決めます。
nWidthnHeightはウィンドウの幅と高さです。クライアント領域の大きさではありません。
hWndParentはこのウィンドウの親ウィンドウのハンドルを渡します。親がいない場合はNULLを渡します。メインウィンドウの場合は通常NULLにします。
hMenuはこのウィンドウに付くメニューハンドルです。ここにNULLが指定されている場合、ウィンドウクラスにしていされたデフォルトメニューが付きます。デフォルトメニューも無い場合は、メニューのないウィンドウになります。
hInstanceはアプリケーションのインスタンスハンドルです。
lpParamはマルチドキュメントインターフェイス(MDI)ウィンドウを作成する時に使用しますが、シングルドキュメントインターフェイスのウィンドウの場合はNULLとします。


 この関数が成功すると、晴れてウィンドウのハンドルを取得する事が出来ます。これは「いつでもウィンドウを表示できますよ」とOSが許可してくれたようなものなのです。失敗するとNULLが返ります。

 取得したウィンドウハンドルを用いれば、ウィンドウの表示は簡単で、

ShowWindow(hWnd, SW_SHOW);

とすると画面に表示されます。

 さて、ここまでの作業、RegisterClassEx関数とCreateWindowEx関数を使っただけで、別に問題はなさそうに思えます。しかし、これをクラス化する時に、実に厄介な問題が生じます。それを次に説明します。


B WinProc関数の壁

 ウィンドウ生成の機構をクラス化して何をしたいかをざっと説明します。ウィンドウクラスを登録した後は、ウィンドウを生成します。作成するウィンドウはアプリケーション毎に機能やスタイルが違うでしょうから、例えばCWindowBaseクラスのようなウィンドウを管理するクラスに受け持ってもらう事になります。スタイルはよいとして、機能の違いとは「受け取ったメッセージに対する処理」の違いです。この処理はクラスの内部で設定された対応するメッセージハンドラ関数にさせます。よって、メッセージハンドラ関数は仮想関数となります。
 ところで、メッセージを最初に受けるのは「グローバル宣言されたWinProc関数」です。よって、上の機構を実現するには、WinProc関数の内部でメッセージの内容を判断し、対応するメッセージハンドラ関数を呼び出します。こうする事で、ウィンドウはメッセージに従って機能するようになります。


 ここまでの流れの何処に問題があるのでしょうか?

 それは「WinProc関数がグローバルである」という点です。グローバル宣言された関数の内部から呼び出せるのは「グローバル宣言された関数」だけです。今、内部で呼び出そうとしているのは「クラスの内部で宣言されたメッセージハンドラ関数」でして、これは仮想関数です。ところが、仮想関数はstatic宣言できないのです。よって、WinProc関数から特定のクラスのメンバ関数を呼び出してメッセージを移譲する部分が実現できません。これがWinProc関数の壁です。

 これを解決する方法は幾つかありますが、一番手っ取り早い方法を取ってみます。まずCWindowBaseクラスのメンバ変数としてLocalWinProc関数をpublic宣言します。これは引数も戻り値もWinProc関数とまったく同じです。この関数の内部でメッセージ処理を行うとします。次に、クラスのヘッダーにCWindowBase(および派生クラス)へのポインタ配列を宣言します。これはstatic宣言です。また、実装部分にこの変数の実体をグローバル変数として定義します。
 次に、CWindowBaseクラスのコンストラクタに「自分自身へのポインタ」をこの配列に追加する文を書きます。こうすると、CWindowBaseオブジェクトが生成される度に配列にポインタが格納されるようになります。
 WinProc関数はこのグローバルな配列からポインタを通してCWindowBaseオブジェクトのLocalWinProc関数を呼び出し、受け取ったメッセージを移譲します。これで、特定のウィンドウに対してメッセージを送信できました。

 この方法が一番「簡単」に思えます。他にもウィンドウクラス登録時に設ける事ができる特別なメモリ領域を利用して、グローバルな部分を隠蔽する事も可能なのですが、結構面倒になります。

 これで一応WinProc関数の問題を解決したとして、最後にメッセージループ機構が待っています。


C メッセージループ

 ウィンドウクラスを登録して、メインウィンドウを作成すると、あとはメッセージを処理する機構のみになります。アプリケーション内で何かメッセージが発生すると、それは「メッセージキュー」にたまります。メッセージキューはアプリケーションに1つしかありません。このメッセージキューからメッセージを1つ取り出すのがGetMessage関数です。

 メッセージを取り出した後、必要であればTranslateMessage関数を用いて仮想キーメッセージを文字メッセージに変換します。もっと重要なのは、次に呼び出すDispatchMessage関数です。この関数は、取り出したメッセージをウィンドウプロシージャに送出します。この時の送出先は引数のMSG構造体に格納されているウィンドウハンドルに対応するウィンドウプロシージャで、OSの内部からコールバックされます。

 ここまでの機構は次の通りです。

// メッセージループ
MSG Msg;

while( GetMessage(&Msg, hWnd, 0, 0) )
{
   TranslateMessage(&MSG);
   DispatchMessage(&MSG);
}

 GetMessage関数はWM_QUITを受け取ると0を返します。それ以外の場合は0以外を返すので、その時はメッセージをウィンドウプロシージャに渡します。

 このループを抜けると、WinMain関数が終わるので、アプリケーションは終了します。


 WinMain関数が始まってからメインウィンドウを作成するまでのプロセスをざっとおさらいしました。クラス化にあたりWinProc関数の壁もなんとか乗り越えられます。ちなみに、この方法は参照文献「アドベンチャーゲームプログラミング」で採用している方法です。これを見たときには「なるほどなぁ」と感心したものです。もちろん別の方法も色々考えられます。

 ここまでの段階だと、まだ単純なウィンドウしか作成できません。アイコンや、カーソル、メニューなどの「リソース」を扱って、初めてウィンドウらしくなります。これらの基本動作については超有名なプログラミングサイトである「猫でもわかるプログラミング」をご覧になるとよく分かると思います。

 ここまでの機構をクラス化する時には、メッセージループなどの大きな流れを管理するアプリケーションクラスと、ウィンドウを専門に扱うCWindowBaseクラスといったように2つに分けると良いでしょうね。MFCではさらにドキュメントクラスというデータを専門に扱うクラスを設けて「ドキュメントビューアーキテクチャ」を実現しています。そこまで実装する必要は無いとは思いますが、少なくともWinMain関数に2〜3行くらい書くだけでメインのウィンドウが表示できるくらいにしておきたいものです。