今回はウィンドウクラスの登録について見ていきます。
RegisterWindowClass()関数
前回紹介したWinMain()関数内からは、まず最初にRegisterWindowClass()関数を呼び出していました。
RegisterWindowClass()関数は同じプロジェクト内の関数です。
// ウィンドウクラスを登録します。
BOOL RegisterWindowClass()
{
// ウィンドウクラスの情報を格納する構造体です。
WNDCLASS stWndClass;
// どんなウィンドウクラスを作るのか、設定をセットします。
stWndClass.style = CS_BYTEALIGNWINDOW | CS_HREDRAW | CS_VREDRAW;
stWndClass.lpfnWndProc = WndProc;
stWndClass.cbClsExtra = 0;
stWndClass.cbWndExtra = 0;
stWndClass.hInstance = g_hInstance;
stWndClass.hIcon = LoadIcon( NULL, IDI_EXCLAMATION );
stWndClass.hCursor = LoadCursor( NULL, IDC_ARROW );
stWndClass.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
stWndClass.lpszMenuName = NULL;
stWndClass.lpszClassName = CLASS_NAME;
// ウィンドウクラスを登録します。
if( RegisterClass( &stWndClass ) == 0 )
{
return FALSE;
}
return TRUE;
}
//////////////////////////////////////////////////////////////////// // ウィンドウクラスを登録します。 BOOL RegisterWindowClass() { // ウィンドウクラスの情報を格納する構造体です。 WNDCLASS stWndClass; // どんなウィンドウクラスを作るのか、設定をセットします。 stWndClass.style = CS_BYTEALIGNWINDOW | CS_HREDRAW | CS_VREDRAW; stWndClass.lpfnWndProc = WndProc; stWndClass.cbClsExtra = 0; stWndClass.cbWndExtra = 0; stWndClass.hInstance = g_hInstance; stWndClass.hIcon = LoadIcon( NULL, IDI_EXCLAMATION ); stWndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); stWndClass.hbrBackground = (HBRUSH)COLOR_BACKGROUND; stWndClass.lpszMenuName = NULL; stWndClass.lpszClassName = CLASS_NAME; // ウィンドウクラスを登録します。 if( RegisterClass( &stWndClass ) == 0 ) { return FALSE; } return TRUE; }
この関数では、「ウィンドウクラスの登録」というとても重要なことを行っています。
ウィンドウクラスの登録
ウィンドウを表示するためには、必ず「ウィンドウクラスの登録」が必要になります。この「ウィンドウクラス」というものについて見ていきましょう。
まず、「ウィンドウクラス」と、「クラス」という名前が付いていますが、C++言語の「クラス」とは全くの別物です。何の関連もありません。また、この「クラス」という言葉には特に意味はありません。
ですので、この用語は「ウィンドウクラス」というひとつの単語として覚えておきましょう。
では「ウィンドウクラス」とは何なのかというと、「これこれこういう形のウィンドウですよ」という、Windowsに登録されたウィンドウの形状のことです。
ウィンドウを作成する際には、まず大まかな形状をWindowsに登録します。これが「ウィンドウクラス」です。登録後、それを元にウィンドウを表示します。つまり、ウィンドウクラスはウィンドウにおけるテンプレートのようなものということです。
ただし、ウィンドウクラスで登録する設定はわずかです。簡単な形状と、アイコンやカーソル、後で説明する「ウィンドウプロシージャ」の設定を行うだけです。そのため、ウィンドウの細かい形状はウィンドウの表示時に設定しますので、ウィンドウクラスで行う設定は、ウィンドウの設定全体の内のごく一部ということになります。
ウィンドウクラスの登録方法
ウィンドウクラスの登録は、RegisterClass()関数というAPIで行います。
まずWNDCLASS構造体に、ウィンドウクラスの情報を設定します。この構造体はウィンドウクラスの情報を格納する構造体で、この中に設定をセットして、RegisterClass()関数に渡すことでウィンドウクラスを登録します。
WNDCLASS構造体のメンバ変数は以下の通りです。
・style:ウィンドウのスタイルを渡します。ただ、本当におおまかな形状しか決めないため、サンプルプログラム1の通りにして構いません。
・lpfnWndProc:「ウィンドウプロシージャ」のアドレスを渡します。詳しくは「ウィンドウプロシージャ」のページで説明します。
・cbClsExtra:ウィンドウクラスの「拡張領域」のサイズ(バイト数)を渡します。通常は0を設定します。
・cbWndExtra:ウィンドウの「拡張領域」のサイズ(バイト数)を渡します。通常は0を設定します。
・hInstance:インスタンスハンドルを渡します。「インスタンスハンドル」についてはすべてはWinMain()から」をご覧ください。
・hIcon:アイコンのハンドルを渡します。詳しくは次項で説明します。
・hCursor:カーソルのハンドルを渡します。詳しくは次項で説明します。
・hbrBackground:背景を塗りつぶすブラシのハンドルを渡します。詳しくは「デバイスコンテキストとハンドル」で説明します。
・lpszMenuName:メニューのハンドルを渡します。詳しくは次項で説明します。
・lpszClassName:ウィンドウクラス名を渡します。あとでウィンドウを作成するときに、この名前を指定します。
ウィンドウクラスの設定は、他のページで説明している部分が多いのでそちらもご覧ください。
正直なところ、ウィンドウクラスはアプリケーションごとにあまり変える必要はありません。特にウィンドウがひとつだけの場合には、これをそのまま使用しても問題ないでしょう。アイコンの設定等はウィンドウの作成後も変えられるからです。
さまざまな種類のウィンドウを表示する場合には、その数だけウィンドウクラスの登録が必要かもしれません。ウィンドウプロシージャやメニューが異なるウィンドウを作る場合には、ウィンドウクラスをそれぞれのウィンドウ用に登録する必要があります。その場合には、lpfnWndProcメンバ変数やlpszMenuName、そしてlpszClassNameメンバ変数をそれぞれ変えて登録すればいいでしょう。
WNDCLASS構造体の準備ができたら、RegisterClass()関数でウィンドウクラスを登録します。
RegisterClass()関数にはWNDCLASS構造体のアドレスを渡します。戻り値は、登録に成功したら0以外、失敗したら0が返されます。
登録したウィンドウクラスは、「ウィンドウクラス名」で区別されます。これはWNDCLASS構造体のlpszClassNameメンバ変数で指定した文字列です。サンプルプログラム1では「CLASS_NAME」を指定していますが、これは「すべてはWinMain()から」で説明した「CLASS_NAMEグローバル変数」で、"MxA02WndClass"という文字列を指定しています。
このウィンドウクラス名は、後でウィンドウを作成する際に「どのウィンドウクラスを元にウィンドウを作成するのか」を指定する時に使用します。
そのため、複数のウィンドウクラスを登録する場合、それぞれのウィンドウクラス名はアプリケーション内で重複しないようにする必要があります。複数のウィンドウクラスを登録する場合にはウィンドウクラス名をそれぞれ別にしましょう。
ただし、ウィンドウクラス名は「アプリケーション内」で重複しなければ問題ありません。普通に登録したウィンドウクラスは「ローカル」なものなので、他のアプリケーションで登録したウィンドウクラス名と重複しても大丈夫です(ただし、ユーティリティアプリケーションの中には「ウィンドウクラス名を見てアプリケーションを識別する」ものとかもあるので、他になさそうなウィンドウクラス名にした方が無難でしょう)。
リソースとは
WNDCLASS構造体で指定するメンバ変数のうち、hIcon、hCursor、lpszMenuNameで指定するアイコン、カーソル、メニューは「リソース」というものです。
「リソース(resource)」というと、一般的には「資源」という意味ですが、Windowsアプリケーションでは、アプリケーション内に埋め込まれた、アイコンやメニュー、ダイアログといった画面表示用データの事を指します。
ウィンドウの左上に表示されるアイコン、ウィンドウ上部に表示されるメニュー、ダイアログ内のボタンやエディットボックスのサイズと位置は、アプリケーション内に「リソース」という形で保存されています。これらは「画面の表示」を行う部分のため、プログラム内で直接記述せず、専用のエディタを使用して作成します。「ダイアログ上のボタンの位置は(2,3)の位置で(100,20)のサイズ」のような指定をプログラム上で行うのは面倒なので、分かりやすいエディタで修正し、それをリソースという形で埋め込めるようになっているわけです。
今回の解説では、本筋とは少し異なるため、リソースは作成していません。
たとえば、ウィンドウクラスで指定しているアイコンは、Windowsシステムが持っている「ビックリマークアイコン」です。
stWndClass.hIcon = LoadIcon( NULL, IDI_EXCLAMATION );
LoadIcon()関数は、アイコンを読み込むAPIです。第1引数にはインスタンスハンドル、第2引数にはアイコンの「ID」を指定します。リソースには「ID」という番号が割り振られていて、読み込むアイコンをその番号で指定します。
ここでは、Windowsシステムのアイコンを使うため、第1引数にはNULLを指定します。その場合、第2引数に指定できるIDはMSDN Win32 API LoadIcon() リファレンスに書かれているものから選択します。
戻り値には「アイコンのハンドル」という、ポインタの一種が返されます。「ハンドル」については「デバイスコンテキストとハンドル」で解説します。リソースはそれぞれハンドルで管理し、アイコンはアイコンのハンドル、メニューはメニューのハンドルを使用することになります。
ちょっと脱線して、リソースのうち、アイコンを作ってみましょう(今回の本筋とは異なるものですのでおまけ扱いです。ここに書かれていることはしなくても構いません。また、実際に修正を行ったサンプルプロジェクトを用意しましたのでそちらもご利用ください)。
リソースを作るためには、まず「リソーススクリプト」を作ります。
リソーススクリプトは「アイコンにどのファイルを使うか」「メニューの構造はどうするか」「ダイアログの中身はどうするか」といったことが書かれたテキストファイルです。
Visual C++のメニューから「ファイル」-「新規作製」を選択します。
「新規作成」ダイアログ(図3)が表示されるので、そのダイアログの上部にあるタブから「ファイル」を選びます(おそらく初めから表示されています)。
ファイルの種類一覧から「リソース スクリプト」を選択し、「ファイル名」に「Script.rc」を入力してください。
OKボタンを押すと、フォルダ表示のような形式でScript.rcが開きますのでこれを閉じてください。これでリソーススクリプトが作成されました。
次にアイコンを作成しましょう。
Visual C++のメニューから「挿入」-「リソース」を選択します。
「リソースの挿入」ダイアログが表示されるので、「リソースのタイプ」から「Icon」を選択して「新規作成」を押してください。アイコンが作成され、アイコンエディタでそのアイコンが開きます。
適当に描いて保存するとアイコンの完成です。
このアイコンのIDは「IDI_ICON1」となります。このIDを分かりやすく変えたい場合には、アイコンエディタで開いた状態で、メニューの「表示」-「プロパティ」を選択して、表示されたプロパティダイアログで変更することができます(今回の例では変更しません)。
このIDは、前述したとおり「整数値」です。「IDI_ICON1」と整数値との結びつけは、「resource.h」というヘッダーファイルで定義されています。このファイルはリソーススクリプトを作ると自動的に作成され、リソースを修正すると自動的に修正されます。
基本的にresource.hの中身は見る必要はありません。修正する必要がありませんし、修正してはいけないわけですから。そのため、基本的に開くことはありませんが、一応ここで見ておきましょう。
// Microsoft Developer Studio generated include file.
// Used by Script.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by Script.rc // #define IDI_ICON1 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1000 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
#defineマクロで、「IDI_ICON1」が「101」という整数値に結びつけられていることが分かると思います。
では、このアイコンをウィンドウの左上で表示してみましょう。
前述したとおり、アイコンはウィンドウクラスを登録する際にWNDCLASS構造体のhIconメンバ変数で指定します。
stWndClass.hIcon = LoadIcon( g_hInstance, MAKEINTRESOURCE( IDI_ICON1 ) );
同じようにLoadIcon()関数を使用しますが、引数が異なります。
第1引数にはインスタンスハンドルを渡します。リソースはアプリケーション内に格納されているので、このアプリケーションのインスタンスハンドルを渡す必要があります。
第2引数には、表示するアイコンのIDを指定します。ただし、直接渡すのではなくMAKEINTRESOURCEマクロを通して渡します。
このようにして、先ほど作成したIDI_ICON1アイコンを読み込み、ウィンドウクラスにセットします。このウィンドウクラスを元に作成したウィンドウは、左上のアイコンがこのIDI_ICON1アイコンになります。
ただ、この「IDI_ICON1」という名称をプログラム中で使うためには、resource.hヘッダーファイルに書かれた定義を読み込んでおく必要があります。そのため、WinMain.cソースファイルの先頭でresource.hヘッダーファイルをインクルードしておきます。
#include <windows.h>
#include "resource.h" // ←追加。
#include "WinMain.h"
// WinMain.c #include <windows.h> #include "resource.h" // ←追加。 #include "WinMain.h"
プログラムの修正は以上です。ビルドして実行すれば、ウィンドウの左上にアイコンが表示されます。メニュー等、他のリソースを使う場合も同じようにしましょう。
他のウィンドウのウィンドウクラス
ウィンドウクラスをMFCで登録する場合について説明する前に、「他のウィンドウ」がどのようなウィンドウクラスなのかということを紹介しておきます。
実は、他のアプリケーションが作成した各ウィンドウの情報や、そのウィンドウクラスの情報を取得することができます(ちなみにそういうプログラムを作ることもできます)。
そのためのツールがVisual C++に付属しています。「Spy++」といって、各アプリケーションや各ウィンドウの情報を取得したり、特定のウィンドウに送られる「メッセージ」を見ることができます(「メッセージ」については「ウィンドウプロシージャ」で説明します)。
このSpy++を実行してみましょう。
まず、ウィンドウを調べる対象用に、フォルダを4つほど開いておいてください。
次に、Visual C++のメニューの「ツール」-「Spy++」を選択してください。Spy++が起動します。
起動直後、Spy++にはウィンドウ一覧がツリー形式で表示されています。
各行は、左から「ウィンドウハンドル(ウィンドウのポインタ)」「ウィンドウ名」「ウィンドウクラス名」となります。
このウィンドウ一覧から「フォルダのウィンドウ」を探してみてください。「フォルダのウィンドウ」は、ウィンドウタイトルが「フォルダ名」になっているので、それを手がかりにして探してみてください。
そのウィンドウのウィンドウクラス名には「CabinetWClass」と書かれていることがわかると思います。このウィンドウクラス名は、どのフォルダも同じ名前になっています。
つまり、「フォルダ」というアプリケーション(つまり「C:\WinNT\explorer.exe」)のウィンドウは、全て同じ「CabinetWClass」という名前のウィンドウクラスを使用しているということです。
また、このツリーを開いていくと、色々なウィンドウが出てくると思います。
実は、ウィンドウは「入れ子」になっています。たとえば「フォルダのウィンドウ」の場合、一番外枠のウィンドウの中に、ツールバーを表示するウィンドウや、ファイル一覧を表示するウィンドウなど、様々なウィンドウが重なっています。
さらにツリーを展開していくと、「Edit」というウィンドウクラスのウィンドウがあると思います。このウィンドウは、「アドレス」の右にある「フォルダのパスを入力する入力欄」のウィンドウです。
実は、Windowsの標準的な画面パーツ――エディットボックス、コンボボックス、ボタン、リスト等――は、全て「ウィンドウの一種」です。一見ただのパーツのように見えますが、実はちゃんとしたウィンドウです。つまり、たとえばダイアログであれば「ダイアログのウィンドウの上に、ボタンやエディットボックスのウィンドウが乗せてある」ということになります。
そして、これらのウィンドウはWindowsがあらかじめウィンドウクラスを登録してあります。アドレスの右側にあるエディットボックスは、「Edit」というウィンドウクラス名で登録されています。
つまり、エディットボックスのようなものを「プログラムで一から作る必要はない」ということです。これらはWindowsがあらかじめ用意してくれているので簡単に表示できるわけです。
このように、ウィンドウクラスには自分で登録するものだけではなく、あらかじめ登録されているものもあるということを憶えておいてください。
MFCのウィンドウクラス
MFCの場合、ウィンドウクラスの登録は複雑な仕組みになっています。
MFCは「ドキュメント・ビュー・アーキテクチャー」というシステムを使用しています。
このシステムは、「ファイル形式を元にビュー(ウィンドウ)を作製する」という方法を採っています。たとえば、テキストファイルを読み込めばテキストを表示し、画像を読み込めば画像を表示し、といったことができる仕組みになっています。
具体的な流れを追ってみましょう。
各アプリケーションのCWinApp派生クラス(たとえばCTest01Appクラスとか)のInitInstance()メンバ関数がまず呼び出されます。
その中でCWinAppクラスのProcessShellCommand()メンバ関数を呼び出し、そこから流れ流れてウィンドウを作成します。
このProcessShellCommand()メンバ関数は、アプリケーションが実行されたときのコマンドライン引数をチェックし、何も指定されていなかったら空のビューとドキュメントを、ファイルが指定されていたらそのファイルを読み込んでドキュメントとビューを表示します。
こういった状況に合わせてウィンドウを作製するため、MFCがウィンドウを作成するときには、その場の状況に合わせてウィンドウクラスを登録します。
そのため、実はウィンドウクラス名がかなり特殊なものになっています。
図5をご覧ください。これはMDIアプリケーションのウィンドウをSpy++で見たものです。
ウィンドウクラス名が「Afx:400000:8:10013:0:950e07」となっています。なんだかかなりへんてこなウィンドウクラス名になっています。
MFCは、何度も新しいウィンドウクラスを登録しなければいけません。そのたびに「何か意味のあるウィンドウクラス名」を付けると面倒だと思ったのか、このようなへんてこなウィンドウクラス名を付けています。
ちなみにこのウィンドウクラス名は、WNDCLASS構造体の各メンバ変数の値を元に作られています。このウィンドウクラス名の作成は、MFCのAfxRegisterWndClass()関数で行っています。
(UINT)hCursor, (UINT)hbrBackground, (UINT)hIcon);
wsprintf(lpszName, _T("Afx:%x:%x:%x:%x:%x"), (UINT)hInst, nClassStyle, (UINT)hCursor, (UINT)hbrBackground, (UINT)hIcon);
このようにアイコンやカーソルのハンドルを元に作成しています。ハンドルはポインタの一種ですから、実行するたびに値が変わるため、実質ランダムな値を元にウィンドウクラス名が作られると言っていいでしょう。
ドキュメント・ビュー・アーキテクチャーを使用した場合、CWinAppクラスのProcessShellCommand()メンバ関数から様々なメンバ関数を経由して、やがてこのAfxRegisterWndClass()関数が呼び出されます。そしてここでウィンドウクラス名が生成されたあと、さらにAfxRegisterClass()関数が呼び出されます。
そして、このAfxRegisterClass()関数の中で、APIのRegisterClass()関数が呼び出され、ウィンドウクラスが登録されます。
{
// 略
if (!::RegisterClass(lpWndClass))
// 略
}
BOOL AFXAPI AfxRegisterClass(WNDCLASS* lpWndClass) { // 略 if (!::RegisterClass(lpWndClass)) // 略 }
つまり「MFCを使用している場合でも、ウィンドウクラスの登録はRegisterClass()関数で行う」ということです。
これはMFCの大原則です。MFCが「Windowsで動くプログラム」である以上、Windowsの操作には必ずAPIを呼び出す必要があります。
そのため、最初はWinMain()関数から始まり、ウィンドウクラスの登録にはRegisterClass()関数を呼び出し、さらにウィンドウの表示にはそのためのAPIを、画面の描画にはそのためのAPIを使用するわけです。
MFCはAPIの呼び出しを隠しているだけです。やっていることは、この解説のサンプルプロジェクトで行っていることが複雑になっただけです。
このことはしっかり憶えておいてください。この点について、次ページ以降も見ていくことになります。
ちなみに、MFCを使用する場合、このドキュメント・ビュー・アーキテクチャーを必ず使用しなければいけないわけではありません。MFCにはこれを使用せずにウィンドウを構築する方法も用意してくれています。
詳しくは「テクニカル ノート 1: ウィンドウ クラスの登録」と、このCodianの「MFCチップス」にある「常駐型アプリケーション」のページをお読みください。
この方法を使用すると、自分の好きなウィンドウクラス名を使用できるため、ウィンドウクラス名が重要という場合にはこちらの方法がいいでしょう。逆に言うと、MFCのドキュメント・ビュー・アーキテクチャーを使用する場合には、ウィンドウクラス名は変更できないと考えた方がいいでしょう。
まとめ
まず「ウィンドウを作製する前にウィンドウクラスをWindowsに登録する必要がある」ということを必ず憶えておいてください。
その際には、WNDCLASS構造体に各設定を渡して、RegisterClass()関数を使って登録します。こちらは忘れても構いません。実際に使うことはあまりないでしょうから。
MFCの部分については、「MFCだって、結局APIを使っているんだ」ということを必ず憶えておいてください。この講座の核心は、まさにここにあります。