ウィンドウのあるプログラム作成 ~ Hello, world 以前

いよいよウィンドウのあるプログラムを作りましょう

ここで作るプログラムの外観はこのようなものです。

そうです、フレームだけで真っ白のウィンドウです。"Hello, world" というメッセージすらありません。

まさに "Hello, world" 以前です・・・。

ですが、それでもいろいろ新しいトピックが登場してきますので、まずはこの枠組みだけをしっかり理解しましょう。

そして、次回、そこに Hello, world! と表示することを目指しましょう。


プログラムの概要

このプログラムはざっくり言って、次のような流れになります。

  1. WinMain からプログラムが始まる
  2. ウィンドウクラスを定義する
  3. ウィンドウクラスをシステムに登録する
  4. システムに登録されたウィンドウクラスを基にウィンドウを生成する
  5. ウィンドウを表示する
  6. メッセージキューからメッセージを取り出して処理する
  7. メッセージの処理を延々と続ける
  8. WM_QUIT メッセージを受け取ったらメッセージの取り出し&処理の繰り返しを終了する
  9. WinMain から抜け、プログラムが終了する

ウィンドウクラスの定義と登録

ウィンドウを作成する前に、「どのようなウィンドウを作るか」定義しなければなりません。

その定義をウィンドウクラス (Window Class) といいます。

ウィンドウクラスの定義には、ウィンドウメッセージを処理する関数、ウィンドウのスタイル (アイコン、メニュー、カーソル等)、 属性、名前等が含まれます。

ウィンドウクラスを定義したら、次にそれをシステムに登録します。ウィンドウは、登録されたウィンドウクラスを基に作成されます。

ウィンドウクラスは次のように分類できます。

システムクラスButton, Edit などの OS によって事前定義されているウィンドウクラス
アプリケーション
グローバル
ウィンドウクラス
複数のプロセス間で利用可能なウィンドウクラス。AppInit_DLLs を利用してプロセスにロードした DLL にてウィンドウクラスを登録する。
アプリケーション
ローカル
ウィンドウクラス
ひとつのプロセス内で利用されるウィンドウクラス。プロセス終了時に登録解除される。
この資料で扱うタイプはこの種類。

この資料では「アプリケーションローカルウィンドウクラス」を作成します。

前述の通り、ウィンドウクラスの定義の中には、そのウィンドウに送られたメッセージを処理する関数が含まれます。 この関数を、ウィンドウプロシージャ(Window Procedure) といいます。

ウィンドウの作成と表示

ウィンドウクラスを定義し、システムにそれを登録したら、そのクラスに属するウィンドウを作成できます。

ウィンドウを作成しただけでは表示されないので、ShowWindow 及び UpdateWindow を呼び、作成したウィンドウを表示します。

メッセージキューからメッセージを取り出して処理し続ける...

ウィンドウを作成すると、ウィンドウを作成したスレッドにひとつのメッセージキューが関連付けされます。 例えばそのウィンドウ上でマウスを動かす等、何らかのイベントが発生すると、そのイベントに応じたパラメータを持つ ウィンドウ メッセージ (または単に「メッセージ」といいます) がメッセージキューに置かれます。

メッセージは実際には整数の値です。Windows SDK では整数の値そのままで使うのではなく、WM_ から始まるシンボルで定義されます。 例えば、終了メッセージは WM_QUIT ですが実際の値は 18 (0x0012) です。どのメッセージがどのような値であるかは、通常はあまり 気にする必要はありません。もし確認したいなら WinUser.h をみれば書いていますのでわかります。

プログラムは、そのメッセージキューを監視しておき、メッセージキューにメッセージが置かれ次第、 そこからメッセージを取り出し、そのメッセージに応じた処理を行います。

メッセージの処理はそのウィンドウに関連付くウィンドウプロシージャで行われます。 特定のメッセージを処理するコードをメッセージハンドラ (Message Handler) といいます。

イベントが発生 → メッセージがメッセージキューに置かれる → メッセージをキューから取り出し処理

これを繰り返します。この処理の繰り返しをメッセージループといいます。

このように、イベントが発生してそのメッセージを次々処理していく形でプログラムが動作します。このように、 イベントがあって始めてプログラムが動作する形式であるため、Windows のウィンドウ付きプログラムはイベントドリブン (イベント駆動) である、といいます。

ウィンドウが破棄されるとき、WM_DESTROY メッセージがシステムからプログラムに送られます。 WM_DESTROY のイベントハンドラで、プログラムの終了を意味する WM_QUIT メッセージを送ります。 通常、WM_QUIT メッセージを受け取ったら、必要なクリーンアップ処理をしてからプログラムを終了します。

プログラムの概要は以上です。おさらいすると下図のようになります。


図1. プログラム概要

いかがでしょうか?概要は把握できましたか?

実装方法

概要がつかめたところで、実際にプログラムを書いていきましょう。

もう一度ざっと繰り返すと、どんなウィンドウにするのか、ウィンドウクラスとして定義しシステムに登録。 登録されたウィンドウクラスから、実際にウィンドウを作成。メッセージを処理するループに入る。 終了メッセージを受け取ったら、WinMain を抜けてプログラム終了。という流れです。

この処理を次のような API 関数、データ構造で実行します。

ウィンドウクラスの定義WNDCLASSEX 構造体にデータをセットする
ウィンドウクラスの登録RegisterClassEx 関数に WNDCLASSEX を渡す
ウィンドウの作成CreateWindowEx 関数にウィンドウクラス名を渡してウィンドウを作る。

正常に作成されるとウィンドウを識別するウィンドウハンドルが取得できる。
ウィンドウの表示ウィンドウハンドルを指定して ShowWindow 及び UpdateWindow 関数を呼び出す。
メッセージを取り出すGetMessage 関数で取り出し、TranslateMessage 関数でその内容を (必要な場合に解釈して) DispatchMessage 関数で WindowProc に渡す

ディスパッチする先を指定しなくて良いのは、ウィンドウにはウィンドウプロシージャが関連付けされているからです。 (メッセージにはウィンドウの識別フィールドがあります)

メッセージの処理を繰り返し、WM_QUIT 受け取り時に繰り返しをやめるGetMessage を while ループで繰り返す。 WM_QUIT メッセージを受け取ると、GetMessage は 0 (FALSE) を返すので、それでループを終了する。

メッセージの受け取りを繰り返すループを特に、メッセージループといいます。

出来上がりのコードはこのようになります。これを simple.cpp として保存しておいて下さい。

#include <windows.h>


#define WND_CLASS_NAME TEXT("My_Window")


LRESULT CALLBACK WindowProc( HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam);


int WINAPI WinMain(
     HINSTANCE hInstance, 
     HINSTANCE hPrevInstance, 
     LPSTR lpCmdLine, 
     int nCmdShow ) {

     HWND hWnd;
     WNDCLASSEX wcl;
     
     wcl.cbSize = sizeof(WNDCLASSEX);
     wcl.hInstance = hInstance;
     wcl.lpszClassName = WND_CLASS_NAME;
     wcl.lpfnWndProc = WindowProc;
     wcl.style = NULL;
     wcl.hIcon = NULL;
     wcl.hIconSm = NULL;
     wcl.hCursor = LoadCursor(NULL, IDC_ARROW);
     wcl.lpszMenuName = NULL;
     wcl.cbClsExtra = 0;
     wcl.cbWndExtra = 0;
     wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
     
     if(!RegisterClassEx(&wcl)) {
          return FALSE;
     }
     
     hWnd = CreateWindowEx(
          NULL, 
          WND_CLASS_NAME,
          TEXT("Windows Programming Primer - Simple"),
          WS_OVERLAPPEDWINDOW,
          CW_USEDEFAULT, 
          CW_USEDEFAULT,
          CW_USEDEFAULT,
          CW_USEDEFAULT,
          NULL,
          NULL,
          hInstance,
          NULL);
     
     if(!hWnd) {
          return FALSE;
     }
     
     ShowWindow(hWnd, nCmdShow);
     
     MSG msg;
     BOOL bRet;
     
     while( ( bRet = GetMessage( &msg, hWnd, 0, 0 ) ) != 0) { 

          if (bRet == -1) {
               break;
          }
          else {
               DispatchMessage( &msg ); 
          }
     }

     return msg.wParam;
     
}


LRESULT CALLBACK WindowProc(
     HWND hwnd,
     UINT uMsg,
     WPARAM wParam,
     LPARAM lParam) {

     switch( uMsg ) {
     case WM_DESTROY:
          ::PostQuitMessage( 0 );
          break;
     default:
          return DefWindowProc(hwnd, uMsg, wParam, lParam);
     }

     return 0;
}

makefile はこのようになります。user32.lib と gdi32.lib をリンクするところに注意してください。

TARGETNAME=simple
LINK32=link.exe
OUTDIR=.\chk

ALL : "$(OUTDIR)\$(TARGETNAME).exe"


"$(OUTDIR)" :
    if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"


CPP_PROJ=\
	/nologo\
	/MT\
	/W3\
	/Fo"$(OUTDIR)\\"\
	/Fd"$(OUTDIR)\\"\
	/c\
	/Zi\
	/DWIN32\
	/DUNICODE\
	/D_UNICODE


LINK32_FLAGS=\
	user32.lib\
	gdi32.lib\
	/nologo\
	/subsystem:windows\
	/pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
	/machine:I386\
	/out:"$(OUTDIR)\$(TARGETNAME).exe"\
	/DEBUG\
	/RELEASE

	
LINK32_OBJS= \
	"$(OUTDIR)\$(TARGETNAME).obj"


"$(OUTDIR)\$(TARGETNAME).exe" : "$(OUTDIR)" $(LINK32_OBJS)
    $(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)


.cpp{$(OUTDIR)}.obj:
   $(CPP) $(CPP_PROJ) $< 


上記の simple.cpp と makefile を同一ディレクトリに保存して、Visual Studio コマンドプロンプトから nmake すればビルドできるはずです。

それでは simple.cpp の内容を一つ一つ見ていきましょう。

まずは windows.h ヘッダファイルの取り込みです。これはほぼ全ての Windows プログラムで行います。


#include <windows.h>

これから作るウィンドウクラスの名前です。何でも構いません。ここでは "My_Window" という文字列にしています。


#define WND_CLASS_NAME TEXT("My_Window")

次はウィンドウプロシージャのプロトタイプです。


LRESULT CALLBACK WindowProc( HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam);

型が LRESULT, 修飾子が CALLBACK です。これはなんでしょうか。 また、引数には HWND, UNIT, WPARAM, LPARAM という型も出てきます。

LRESULT は LONG_PTR 型、要はポインタ型。

CALLBACK は __stdcall です。WINAPI と同じです。

HWND はウィンドウハンドルです。H はハンドルの意味を表しています。HWND で WND (Window) の H (Handle) です。 その他、例えば HINSTANCE は INSTNACE の H (インスタンスのハンドル) です。

Windows API では 「なになにハンドル」という言葉が良く出てきますので覚えておいて下さい。 ハンドルは通常システムワイドの識別子です。型は HANDLE で定義されており、実際には void* として定義されています。

UNIT は符号なし INT。 WPARAM, LPARAM は共に UINT_PTR です。こちらもポインタ型。実質的に 32 ビット環境では、 4バイトのデータ、と考えておけばよいでしょう。

WinMain に入り、直後に WNDCLASSEX に値をセットしています。これはウィンドウクラスの定義です。


int WINAPI WinMain(
	HINSTANCE hInstance, 
	HINSTANCE hPrevInstance, 
	LPSTR lpCmdLine, 
	int nCmdShow ) {

	HWND hWnd;
	WNDCLASSEX wcl;
	
	wcl.cbSize = sizeof(WNDCLASSEX);
	wcl.hInstance = hInstance;
	wcl.lpszClassName = WND_CLASS_NAME;
	wcl.lpfnWndProc = WindowProc;
	wcl.style = NULL;
	wcl.hIcon = NULL;
	wcl.hIconSm = NULL;
	wcl.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcl.lpszMenuName = NULL;
	wcl.cbClsExtra = 0;
	wcl.cbWndExtra = 0;
	wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
	

ウィンドウクラスが定義できたら、次にそれを RegisterClassEx でシステムに登録します。

	
	if(!RegisterClassEx(&wcl)) {
		return FALSE;
	}
	

上で登録したウィンドウクラスのウィンドウを作成します。

	
	hWnd = CreateWindowEx(
		NULL, 
		WND_CLASS_NAME,
		TEXT("Windows Programming Primer - Simple"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL);
	
	if(!hWnd) {
		return FALSE;
	}
	

ここで重要なのは第2引数と、第4引数です。それぞれ、ウィンドウクラス名、ウィンドウスタイルを表します。 ウィンドウクラスは、上で定義したウィンドウクラスの名前を指定します。

ウィンドウスタイルは、 ここでは「普通のタイトルバーと縁のあるウィンドウ」を作りたいので、WS_OVERLAPPEDWINDOW という値を渡します。 (完全なウィンドウスタイルのリストは MSDN を参照してください) 具体的には WS_OVERLAPPEDWINDOW の値は WS_OVERLAPPED、WS_CAPTION、WS_SYSMENU、WS_THICKFRAME、WS_MINIMIZEBOX、WS_MAXIMIZEBOX の和 (組み合わせ) で、 (これらの値は WinUser.h に記載されています) タイトルバー、キャプション、普通のメニュー、枠、最小化・最大化ボタン等を持ちます。

他のパラメータはここでは特に重要ではありません。サンプルの通りにしておいて下さい。 デフォルトのウィンドウの場所ということになります。

	
	ShowWindow(hWnd, nCmdShow);
	

ShowWindow によって、「hWnd で指定したウィンドウを表示する」という指示を出します。

次がメッセージループです。

	
	MSG msg;
	BOOL bRet;
	
	while( ( bRet = GetMessage( &msg, hWnd, 0, 0 ) ) != 0) { 

		if (bRet == -1) {
			break;
		}
		else {
			DispatchMessage( &msg ); 
		}
	}

GetMessage でメッセージキューからメッセージを取り出します。メッセージは MSG 構造体で渡されます。 MSG 構造体は以下の形です。

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

GetMessage は WM_QUIT を受け取ったときに 0 を、WM_QUIT 以外のメッセージを取り出したとき非ゼロの値を返します。 (従って while で true と判断される) エラーが発生したときは -1 です。

GetMessage が返ると、msg に値がセットされています。それを、DispatchMessage に渡します。 DispatchMessage 関数は適切な WindowProc を呼び出します。

ウィンドウプロシージャ WindowProc では、下記のコードの通りメッセージが WM_DESTROY 値のときのみ特有の処理を行い、 それ以外のときは、特別な処理をせずに DefWindowProc というデフォルトのウィンドウプロシージャに渡しています。


LRESULT CALLBACK WindowProc(
	HWND hwnd,
	UINT uMsg,
	WPARAM wParam,
	LPARAM lParam) {

	switch( uMsg ) {
	case WM_DESTROY:
		::PostQuitMessage( 0 );
		break;
	default:
		return DefWindowProc(hwnd, uMsg, wParam, lParam);
	}

	return 0;
}

PostQuitMessage はメッセージキューに WM_QUIT メッセージを送り、その応答を待たずに制御を返します。

WM_QUIT メッセージを受け取ったときに、メッセージループは終了します。 また、このとき WinMain は WM_QUIT のときの wParam を返します。


	return msg.wParam;
	
}

以上で、サンプルプログラムの説明は終了です。

いかがでしたか?ウィンドウを作成する流れはつかめましたでしょうか?

パラメータやオプションがたくさんあるので、あまり枝葉にこだわらずに骨組みにこだわるようにしてください。

関連資料

WindowProc の呼ばれ方

当サイト内の資料。ウィンドウプロシージャの呼ばれ方、内部動作を実際にデバッガで検証してみました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説