Tech Notes

CreateDialogとプロシージャの謎

久々にWin32API×C++でダイアログベースのツール系アプリを作ろうとしたら、謎な部分があったのでメモ。 自分はC++を主にゲーム制作とかマイコン制御とかで使ってて、普段ツールっぽいアプリを作るときは大体C#を使います。ダイアログの部品制御とかがC++と比較してラクチンすぎて開発性異常なためです。しかし、今回は久々にC++でやってみたんです。

まあダイアログベースなのでCreateDialogを使うじゃないですか。

だいたいこんな感じの構成のコードになるじゃないですか。

#include <Windows.h>
#include <tchar.h>
#include "resource.h"

INT_PTR CALLBACK DlgProc(...){
	// 処理
}

int WINAPI _tWinMain(...){
	HWND hWnd;
	MSG msg;
	BOOL bRet;

	hWnd = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG), NULL, DlgProc);

	if (hWnd == NULL) {
		return 0;
	}

	ShowWindow(hWnd, nCmdShow);

	while (bRet = GetMessage(&amp;msg, NULL, 0, 0)) {
		if (bRet == -1) {
			break;
		}
		TranslateMessage(&amp;msg);
		DispatchMessage(&amp;msg);
	}
	return (int)msg.wParam;
}

で、ウィンドウクラスを使う場合はこう

#include <Windows.h>
#include <tchar.h>
#include "resource.h"

INT_PTR CALLBACK DlgProc(...){
	// 処理
}

int WINAPI _tWinMain(...){
	HWND hWnd;
	MSG msg;
	WNDCLASSEX winc;
	BOOL bRet;

	winc.cbSize = sizeof(WNDCLASSEX);
	winc.style = //ウィンドウスタイル
	winc.lpfnWndProc = (WNDPROC)DlgProc;
	winc.cbClsExtra = 0;
	winc.cbWndExtra = DLGWINDOWEXTRA;
	winc.hInstance = hInstance;
	winc.hIcon = // アイコン
	winc.hIconSm = NULL;
	winc.hCursor = //カーソル
	winc.hbrBackground = //背景色ブラシ
	winc.lpszMenuName = //メニュー;
	winc.lpszClassName = _T("ウィンドウクラス名");

	if (RegisterClassEx(&amp;winc) == 0) {
		return 0;
	}

	hWnd = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG), NULL, NULL);

	if (hWnd == NULL) {
		return 0;
	}

	ShowWindow(hWnd, nCmdShow);

	while (bRet = GetMessage(&amp;msg, NULL, 0, 0)) {
		if (bRet == -1) {
			break;
		}
		TranslateMessage(&amp;msg);
		DispatchMessage(&amp;msg);
	}
	return (int)msg.wParam;
}

ウィンドウクラスを使う場合は、リソースファイル内でダイアログの記述に「CLASS "ウィンドウクラス名"」みたいなのを加えることでダイアログとウィンドウクラスを関連付けるわけですが、ここからが本題。

前者のソースコードではプロシージャをCreateDialogの第4引数として指定している訳ですが、後者の方ではウィンドウクラスのlpfnWndProcメンバとして指定している訳です。

ここで浮かんでくる疑問が、「後者の方のソースコードでCreateDialogの第4引数の方にプロシージャを指定したらどうなるか?」ということです。

MSDNも見てみましたが、特にこの疑問に関する記述はありません。

実はウィンドウクラスのlpfnWndProcメンバは「WNDPROC」型でCreateDialogの第4引数は「DLGPROC」型になっており、違うものです。後者のコードでDlgProc関数へのポインタをWNDPROC型にキャストしているのはその為で、キャストせずにlpfnWndProcに代入しようとすると型不整合でコンパイラに文句を言われます。逆もまたしかりです。

「ダイアログのプロシージャなんだからWNDPROC型じゃなくてDLGPROC型のままキャストせず利用するのが自然じゃね?」ということでプロシージャをCreateDialogの第4引数に指定し、lpfnWndProcにNULLを入れて実験してみました。

#include <Windows.h>
#include <tchar.h>
#include "resource.h"

INT_PTR CALLBACK DlgProc(
	HWND hWnd,
	UINT msg,
	WPARAM wParam,
	LPARAM lParam)
{
	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, msg, wParam, lParam);
	}
	return 0;
}

int WINAPI _tWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	PTSTR lpCmdLine,
	int nCmdShow)
{
	HWND hWnd;
	MSG msg;
	WNDCLASSEX winc;
	BOOL bRet;

	winc.cbSize = sizeof(WNDCLASSEX);
	winc.style = CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc = NULL;
	winc.cbClsExtra = 0;
	winc.cbWndExtra = DLGWINDOWEXTRA;
	winc.hInstance = hInstance;
	winc.hIcon = NULL;
	winc.hIconSm = NULL;
	winc.hCursor = NULL;
	winc.hbrBackground = NULL;
	winc.lpszMenuName = NULL;
	winc.lpszClassName = _T("WindowClass");

	if (RegisterClassEx(&amp;winc) == 0) {
		return 0;
	}

	hWnd = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG), NULL, DlgProc);

	if (hWnd == NULL) {
		return 0;
	}

	ShowWindow(hWnd, nCmdShow);

	while (bRet = GetMessage(&amp;msg, NULL, 0, 0)) {
		if (bRet == -1) {
			break;
		}
		TranslateMessage(&amp;msg);
		DispatchMessage(&amp;msg);
	}
	return (int)msg.wParam;
}

これは動的エラーが出ます。多分lpfnWndProcのぬるぽを参照したということでしょう。どうやらウィンドウクラスが指定されている場合ならWindowsはCreateDialogの第4引数ではなくウィンドウクラスのlpfnWndProcの方を参照するようです。

どっちを参照するのかという疑問はこれにて解決。終わり、閉廷と行きたいところですがちょっとだけ続きがあります。

一応両方にプロシージャへの関数ポインタを指定した場合について調べてみたんです。

どっちが参照されているのか明確にするためにメッセージボックスを表示させます。

#include <Windows.h>
#include <tchar.h>
#include "resource.h"

INT_PTR CALLBACK DlgProc1(
	HWND hWnd,
	UINT msg,
	WPARAM wParam,
	LPARAM lParam)
{
	switch (msg) {
	case WM_CREATE:
		MessageBox(hWnd, _T("proc1_wm_create"), _T("test"), MB_OK);
		break;
	case WM_INITDIALOG:
		MessageBox(hWnd, _T("proc1_wm_initdialog"), _T("test"), MB_OK);
		return TRUE;
	case WM_DESTROY:
		MessageBox(hWnd, _T("proc1_wm_destroy"), _T("test"), MB_OK);
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, msg, wParam, lParam);
	}
	return 0;
}

INT_PTR CALLBACK DlgProc2(
	HWND hWnd,
	UINT msg,
	WPARAM wParam,
	LPARAM lParam)
{
	switch (msg) {
	case WM_CREATE:
		MessageBox(hWnd, _T("proc2_wm_create"), _T("test"), MB_OK);
		break;
	case WM_INITDIALOG:
		MessageBox(hWnd, _T("proc2_wm_initdialog"), _T("test"), MB_OK);
		return TRUE;
	case WM_DESTROY:
		MessageBox(hWnd, _T("proc2_wm_destroy"), _T("test"), MB_OK);
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, msg, wParam, lParam);
	}
	return 0;
}

int WINAPI _tWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	PTSTR lpCmdLine,
	int nCmdShow)
{
	HWND hWnd;
	MSG msg;
	WNDCLASSEX winc;
	BOOL bRet;

	winc.cbSize = sizeof(WNDCLASSEX);
	winc.style = CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc = (WNDPROC)DlgProc1;
	winc.cbClsExtra = 0;
	winc.cbWndExtra = DLGWINDOWEXTRA;
	winc.hInstance = hInstance;
	winc.hIcon = NULL;
	winc.hIconSm = NULL;
	winc.hCursor = NULL;
	winc.hbrBackground = NULL;
	winc.lpszMenuName = NULL;
	winc.lpszClassName = _T("WindowClass");

	if (RegisterClassEx(&amp;winc) == 0) {
		return 0;
	}

	hWnd = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG), NULL, DlgProc2);

	if (hWnd == NULL) {
		return 0;
	}

	ShowWindow(hWnd, nCmdShow);

	while (bRet = GetMessage(&amp;msg, NULL, 0, 0)) {
		if (bRet == -1) {
			break;
		}
		TranslateMessage(&amp;msg);
		DispatchMessage(&amp;msg);
	}
	return 0;
}

これを実行してみると「proc1_wm_create」「proc1_wm_initdialog」という順で起動後2回メッセージボックスが出ます。ちなみに最初の普通のコード(lpfnWndProc→DlgProc、CreateDialogの第4引数→NULL)の場合はwm_createのみになります。

なぜwm_initdialogも?しかもDlgProc2でなくDlgProc1の方で?という謎が発生しました。

WM_INITDIALOGは最初の2つのコードの内の前者の方においてのみ(つまりウィンドウクラスを使わない場合)発生します。そして後者の場合でwm_createが発生します。しかし、プロシージャを両方指定するとウィンドウメッセージが両方発生するようです。

多分これはいわゆる未定義の動作ではないかと思われます。とりあえず、両方にプロシージャを指定する意味は特にないでしょう。ウィンドウクラスとCreateDialogを使ってダイアログを生成する場合はウィンドウクラスの側にプロシージャを指定しとけということです。