EternalWindows
ウインドウ管理 / メッセージ専用ウインドウ

前節では、メッセージによるプロセス間通信について説明しましたが、 ウインドウをデータの交換という目的だけに使うような場合、 そのウインドウは表示したくないということもあるでしょう。 たとえば、メッセージを受信するサーバーアプリケーションのことを考えた場合、 サーバーは基本的にユーザーインターフェースを備えませんから、 データ交換のために作成したウインドウを表示するわけにはいきません。 非表示のウインドウとして作成し、こっそりとクライアントからの メッセージを処理するべきでしょう。

以前説明したように、CreateWindowExはウインドウスタイルにWS_VISIBLEが 指定されていなければウインドウを非表示として作成しますから、 後はShowWindowを呼ばないようにすれば非表示の状態を維持できるはずです。 しかし、この非表示のウインドウは他のプログラムからの干渉を 完全に遮っているわけではありません。 たとえば、EnumWindowsというウインドウを列挙する関数を呼び出せば、 たとえ非表示のウインドウであろうとハンドルを取得することができてしまいます。 次にEnumWindowsを利用した簡単なプログラムを示します。

#include <windows.h>

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE hFile;

	hFile = CreateFile(TEXT("sample.txt"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	
	EnumWindows(EnumWindowsProc, (LPARAM)hFile);
	
	CloseHandle(hFile);

	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	return 0;
}

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
	TCHAR szBuf[1024];
	TCHAR szClassName[256];
	TCHAR szWindowName[256];
	DWORD dwResult;

	GetWindowText(hwnd, szWindowName, sizeof(szWindowName));
	GetClassName(hwnd, szClassName, sizeof(szClassName));

	wsprintf(szBuf, TEXT("[window] %s [class] %s\r\n"), szWindowName, szClassName);

	WriteFile((HANDLE)lParam, szBuf, lstrlen(szBuf), &dwResult, NULL);
	
	return TRUE;
}

EnumWindowsは、トップレベルウインドウが存在するまで、 もしくは第1引数に示す関数がFALSEを返すまでその関数を呼び続けます。 トップレベルウインドウとは、親を持たないウインドウのことで、 CreateWindowExの第9引数にNULLを指定して作成されたウインドウは、 全てトップレベルウインドウです。 第2引数は第1引数の関数に指定できるパラメータで、 ファイルハンドルを指定しています。 EnumWindowsProcでは、このハンドルを使用して列挙されたウインドウの クラス名とウインドウタイトルをファイルに書き込みます。 表示状態のウインドウのみを書き込みたい場合は、次のようにします。

if (IsWindowVisible(hwnd))
	WriteFile((HANDLE)lParam, szBuf, lstrlen(szBuf), &dwResult, NULL);

IsWindowVisibleは、ウインドウが表示状態であれば0以外の値を返します。 このようにすれば、ファイルに書き込まれるウインドウの数は少なくなるため、 目的のウインドウを探しやすくなります。 たとえば、メモ帳を起動してから上記プログラムを実行すると、 直ぐにメモ帳のウインドウタイトルを発見することができ、 「Notepad」というクラス名を知ることができます。

単純に非表示のウインドウを作成するだけでは、 上記のように意図していない形でウインドウハンドルを取得されてしまいますが、 そのウインドウをメッセージ専用ウインドウとして作成すれば、 他のアプリケーションからの干渉を防ぐことができます。 メッセージ専用ウインドウには、次のような特徴があります。

・メッセージ専用ウインドウは、常に非表示である(たとえ、ShowWindowを呼び出しても)。
・メッセージ専用ウインドウは、EnumWindows等で列挙することができない。
・メッセージ専用ウインドウは、ブロードキャストメッセージを受け取らない。

ブロードキャストメッセージとは、全てのトップレベルウインドウへ一斉に送られるメッセージのことで、 PostMessageやSendMessageの第1引数にHWND_BROADCASTを指定することで実現できます。

PostMessage(HWND_BROADCAST, WM_CLOSE, 0, 0);

このコードは、HWND_BROADCASTを指定してWM_CLOSEをポストしていますから、 全てのトップレベルウインドウをクローズさせようとしています。 しかし、メッセージ専用ウインドウはブロードキャストメッセージを受け取らないため、 このWM_CLOSEがウインドウプロシージャに届くことはありません。 このブロードキャストと先のウインドウの列挙のことから分かるように、 メッセージ専用ウインドウは非常に安全なウインドウであり、 他のアプリケーションからの予期しないメッセージを処理することはありません。

メッセージ専用ウインドウのウインドウハンドルは、 以外にも前節で示したFindWindowで取得できます。 しかし、FindWindowExを呼び出せばより確実に ウインドウハンドルを取得することができると思われます。

HWND FindWindowEx(
    HWND hwndParent,
    HWND hwndChildAfter,
    LPCTSTR lpszClass,
    LPCTSTR lpszWindow
);

この関数は主に子ウインドウを探すために使われるもので、 たとえば、hwndParentに親ウインドウのハンドルを指定して、 hwndChildAfterにNULLを指定すると、ウインドウの検索範囲は、 hwndParentの全ての子ウインドウとなります。 hwndParentにHWND_MESSAGEを指定すると、メッセージ専用ウインドウが検索されます。 次のコードは、前節のFindWindowの呼び出しをFindWindowExに置き換えたものです。

hwndTarget = FindWindowEx(HWND_MESSAGE, NULL, TEXT("receiver"), NULL);

第1引数に、メッセージ専用ウインドウの検索としてHWND_MESSAGEを指定しています。 実際には、この引数をNULLにしてもメッセージ専用ウインドウのハンドルは取得できますが、 HWND_MESSAGEを指定すれば検索の範囲が限定されるという利点があります。 つまり、lpszClassやlpWindowNameと一致するウインドウが存在しても、 それがメッセージ専用ウインドウでなければヒットすることにならないため、 予期していないウインドウのハンドルが返る確率は低くなるわけです。

今回のプログラムは、前節で作成したプログラムからのWM_COPYDATAを受信します。 作成されるウインドウは、あくまでWM_COPYDATAの受信を目的とするだけであり、 ユーザーからの入力には一切応答するつもりがないため、 メッセージ専用ウインドウとして作成されています。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("receiver");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = NULL;
	wc.hCursor       = NULL;
	wc.hbrBackground = NULL;
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = NULL;
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

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

	case WM_COPYDATA: {
		PCOPYDATASTRUCT pData = (PCOPYDATASTRUCT)lParam;

		if (pData->dwData == 1)
			SetWindowText((HWND)wParam, (LPTSTR)pData->lpData);
		else if (pData->dwData == 2) {
			PostMessage(hwnd, WM_CLOSE, 0, 0);
			MessageBeep(0);
		}
		else
			;

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

メッセージ専用ウインドウを作成するには、特別な関数を呼び出す必要はありません。 CreateWindowExの第9引数にHWND_MESSAGEを指定するだけでよいのです。

hwnd = CreateWindowEx(0, szAppName, NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, NULL, hinst, NULL);	

第3引数をNULLにしているのは、メッセージ専用ウインドウが非表示であるため、 ウインドウタイトルを設定しても仕方がないと思ったからです。 また、非表示ということから、ShowWindowやUpdateWindowの呼び出しもカットしています。

WM_COPYDATAでは、COPYDATASTRUCT構造体のdwDataを基に、 行うべき処理を決定しています。

case WM_COPYDATA: {
	PCOPYDATASTRUCT pData = (PCOPYDATASTRUCT)lParam;

	if (pData->dwData == 1)
		SetWindowText((HWND)wParam, (LPTSTR)pData->lpData);
	else if (pData->dwData == 2) {
		PostMessage(hwnd, WM_CLOSE, 0, 0);
		MessageBeep(0);
	}
	else
		;

	return 0;
}

WM_COPYDATAのlParamはCOPYDATASTRUCT構造体のアドレスが格納されているので、 それをCOPYDATASTRUCT構造体のポインタで受け取ります。 dwDataが1のときは文字列を送る設計になっていたので、 LPTSTRでキャストして利用することができます。 今回は、送信側のウインドウタイトルを変更していますから、 送信側はウインドウタイトルの変更からデータが処理されたことを確認できます。 dwDataが2のときは、WM_CLOSEをポストして自分自身を終了させようとしています。 これは、メッセージ専用ウインドウが非表示であるため、 閉じるボタンの押下でアプリケーションを終了できないからです。 MessageBeepでビープ音を鳴らしているのは、データを処理したことを知らせるためです。

WM_COPYDATAの実装背景

WM_COPYDATAを処理する側は、それがSendMessageで送られてくることを意識すべきです。 SendMessageはメッセージを処理するまで制御を返しませんから、 WM_COPYDATAで時間の掛かる処理を行うとその分、送信側は待たされることになります。 ちなみに、受信側がReplayMessageという関数を呼び出せば、 送信側のSendMessageは直ぐに制御を返しますが、この方法は好ましくないと思われます。 そもそも、WM_COPYDATAが別プロセスのウインドウにデータを送れるのは、 内部でファイルマッピングというオブジェクトを作成しているからであり、 SendMessageが制御を返すときにはこれは開放されます。 しかし、ReplayMessageを呼び出した側は、まだファイルマッピングに支持されるメモリを 使用しているかもしれませんから、このタイミングでメモリの開放が行われるとWM_COPYDATAの受信側は アクセス違反を発生させるかもしれません。 これらのことから推測できるように、WM_COPYDATAように内部でメモリを 確保するメッセージをPostMessageではなくSendMessageで送るのには理由があります。 それは、SendMessageでなければ受信側の処理の終了を確認できないため、 確保したメモリを開放するタイミングを知り得るのもこのときしかないからです。 WM_COPYDATAのように内部でメモリを確保するメカニズムは、 WM_SETTEXTやコントロール専用のメッセージなどでも利用されています。



戻る