EternalWindows
ウインドウ管理 / ツールウインドウ

ウインドウの作成やウインドウクラスについて理解するにあたって、ツールウインドウはよい題材になります。 次に、ツールウインドウの例を示します。

Visual StudioでCtrl + Fを押せば、上図のようなウインドウが表示されますが、 このようなキャプションを持つウインドウがツールウインドウです。 ツールウインドウの便利な点は、ウインドウを表示した状態で他のウインドウ上でも作業できる点です。 たとえば、上図のウインドウでは、検索した文字列を別ウインドウで直ちに確認できます。 モードレスダイアログにも同じような特徴があるため、 アプリケーションによってはツールウインドウではなく、モードレスダイアログを作成することもあります。

今回のプログラムは、通常のウインドウ上にボタンを作成し、 それがクリックした場合にツールウインドウを作成します。

#include <windows.h>

#define ID_BUTTON 100

HWND CreateToolWindow(HWND hwndParent, HINSTANCE hinst);
LRESULT CALLBACK ToolWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
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("sample");
	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         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

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

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	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)
{
	static HWND hwndTool = NULL;

	switch (uMsg) {

	case WM_CREATE:
		CreateWindowEx(0, TEXT("BUTTON"), TEXT("ツールウインドウ作成"), WS_CHILD | WS_VISIBLE, 30, 30, 190, 30, hwnd, (HMENU)ID_BUTTON, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		return 0;

	case WM_COMMAND:
		if (LOWORD(wParam) == ID_BUTTON) {
			if (!IsWindow(hwndTool))
				hwndTool = CreateToolWindow(hwnd, (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE));
			else
				SetActiveWindow(hwndTool);

		}
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

HWND CreateToolWindow(HWND hwndParent, HINSTANCE hinst)
{
	TCHAR      szAppName[] = TEXT("toolwindow");
	HWND       hwnd;
	WNDCLASSEX wc;

	if (!GetClassInfoEx(hinst, szAppName, &wc)) {
		wc.cbSize        = sizeof(WNDCLASSEX);
		wc.style         = 0;
		wc.lpfnWndProc   = ToolWindowProc;
		wc.cbClsExtra    = 0;
		wc.cbWndExtra    = 0;
		wc.hInstance     = hinst;
		wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
		wc.hbrBackground = (HBRUSH)(COLOR_MENU + 1);
		wc.lpszMenuName  = NULL;
		wc.lpszClassName = szAppName;
		wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		
		if (RegisterClassEx(&wc) == 0)
			return NULL;
	}
	
	hwnd = CreateWindowEx(WS_EX_TOOLWINDOW, szAppName, szAppName, WS_SYSMENU | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, (HWND)hwndParent, NULL, hinst, NULL);
	if (hwnd == NULL)
		return NULL;

	return hwnd;
}

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

	case WM_CREATE:
		return 0;

	default:
		break;

	}

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

WindowProcのWM_CREATEでは、ツールウインドウを作成するためのコントロールを作成しています。 コントロールは親ウインドウ(今回の場合hwnd)の子ウインドウという位置づけであり、 子ウインドウを作成する場合はウインドウスタイルにWS_CHILDを指定します。 また、第9引数に親ウインドウのハンドルを指定し、第10引数にコントロールのIDを指定します。 第2引数が"BUTTON"であることから、この作成されたコントロールはボタンになり、 クリックされたらWM_COMMANDが送られます。

case WM_COMMAND:
	if (LOWORD(wParam) == ID_BUTTON) {
		if (!IsWindow(hwndTool))
			hwndTool = CreateToolWindow(hwnd, (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE));
		else
			SetActiveWindow(hwndTool);

	}
	return 0;

wParamの下位ワードはコントロールのIDであり、これがID_BUTTONと一致した場合は、ボタンがクリックされたことが分かります。 よって、CreateToolWindowという自作関数でツールウインドウを作成しようとしますが、 既にツールウインドウが作成されている場合は、新たに作成する必要はないはずです。 よって、IsWindowでツールウインドウが存在しないことを確認した場合だけ作成し、 そうでない場合は現在のツールウインドウをSetActiveWindowでアクティブにするだけにしています。 CreateToolWindowの第1引数にhwndを指定しているのは、ツールウインドウの親ウインドウをhwndに設定するためであり、 第2引数にHINSTANCEを指定するのは、それがウインドウクラスの作成で必要になるからです。 WM_CREATEでは、LPARAMのCREATESTRUCT構造体からHINSTANCE参照できましたが、WM_COMMANDではそのような事はできないため、 GetWindowLongPtrでHINSTANCEを取得しています。 CreateToolWindowの内部は、次のようになっています。

HWND CreateToolWindow(HWND hwndParent, HINSTANCE hinst)
{
	TCHAR      szAppName[] = TEXT("toolwindow");
	HWND       hwnd;
	WNDCLASSEX wc;

	if (!GetClassInfoEx(hinst, szAppName, &wc)) {
		wc.cbSize        = sizeof(WNDCLASSEX);
		wc.style         = 0;
		wc.lpfnWndProc   = ToolWindowProc;
		wc.cbClsExtra    = 0;
		wc.cbWndExtra    = 0;
		wc.hInstance     = hinst;
		wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
		wc.hbrBackground = (HBRUSH)(COLOR_MENU + 1);
		wc.lpszMenuName  = NULL;
		wc.lpszClassName = szAppName;
		wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		
		if (RegisterClassEx(&wc) == 0)
			return NULL;
	}
	
	hwnd = CreateWindowEx(WS_EX_TOOLWINDOW, szAppName, szAppName, WS_SYSMENU | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, (HWND)hwndParent, NULL, hinst, NULL);
	if (hwnd == NULL)
		return NULL;

	return hwnd;
}

基本的にコントロール以外のウインドウはウインドウクラスの登録が必要であるため、 WNDCLASSEX構造体を初期化してRegisterClassExを呼び出すことになります。 WinMainのときと比べ、szAppNameが変化している点とwc.lpfnWndProcにToolWindowProcを指定している点が重要です。 このToolWindowProcが、ツールウインドウのウインドウプロシージャになります。 hbrBackgroundにウインドウの背景色となるブラシを指定しますが、 GetStockObjectではツールウインドウとして相応しいブラシを取得することはできません。 CreateSolidBrush(GetSysColor(COLOR_MENU))とすることでブラシを作成することもできますが、 hbrBackgroundに関しては、もっと特別な方法が用意されています。 COLOR_XXXとして定義される定数に+1を指定すれば、その色のブラシを指定したことと同じ意味になるため、 これを使用しています。 ウインドウクラスの登録の処理をGetClassInfoExのif文で囲っているのは、 同じウインドウクラスが登録されている場合は、RegisterClassExが失敗するからです。 GetClassInfoExが失敗する場合は、ウインドウクラスがまだ登録されていないことを意味するため、 このときだけRegisterClassExを呼び出します。 ウインドウクラスの登録が完了したらツールウインドウの作成に入りますが、 これは第1引数にWS_EX_TOOLWINDOWを指定します。 この拡張ウインドウスタイルを指定している場合は、ウインドウスタイルにWS_OVERLAPPEDWINDOWを指定していても、 キャプションに最小化や最大化のボックスが表示されることはありません。 ただし、キャプション上における右クリックからの最小化や最大化は可能になるため、 これを防ぎたい場合はWS_SYSMENU | WS_CAPTIONを指定すればよいでしょう。 WS_VISIBLEも指定しているのは、ウインドウの作成と同時にウインドウを表示するためです。

ToolWindowProcには、ツールウインドウとしての処理を記述することになります。 今回は何も行っていませんが、たとえばVisual Studioのように検索を可能にしたい場合は、 そのためのコントロールを作成することになるでしょう。 WM_DESTROYのPostQuitMessageを呼び出す処理は、メインウインドウでのみ必要なものであるため、 ツールウインドウで実行する必要はありません。

ツールウインドウと子ウインドウ

今回のツールウインドウを作成するCreateWindowExでは、第9引数に親ウインドウハンドルを指定していましたが、 これについて少し気になるところがあるかもしれません。 本来、第9引数にウインドウハンドルを指定した場合は作成するウインドウが子ウインドウとなり、 ウインドウスタイルにWS_CHILDを指定し、第10引数にウインドウのIDを指定しなければならないはずです。 しかし、ツールウインドウに関しては第9引数のウインドウハンドルは親として解釈されるのではなく、 ツールウインドウと連動するウインドウと解釈されるようです。 現に、ツールウインドウのハンドルをGetParentに指定しても、返されるウインドウはNULLになります。 それでは次のように、WS_CHILDとウインドウのIDを指定するとどうなるのでしょう。

hwnd = CreateWindowEx(WS_EX_TOOLWINDOW, szAppName, szAppName, WS_SYSMENU | WS_CAPTION | WS_CHILD | WS_VISIBLE, 100, 100, 300, 200, (HWND)hwndParent, (HMENU)1000, hinst, NULL);

この場合は、ツールウインドウが親ウインドウの外へ移動できなくなります。 一般的にこの動作は使いにくいと思われるため、WS_CHILDとウインドウのIDは指定しないほうがよいと思われます。 ちなみに、この場合に関しては、次の処理を親ウインドウのWindowProcに記述することで、 ツールウインドウが破棄されるタイミングを特定できます。

case WM_PARENTNOTIFY:
	if (LOWORD(wParam) == WM_DESTROY && HIWORD(wParam) == 1000) 
		MessageBox(NULL, TEXT("ツールウインドウが閉じられます。"), TEXT("OK"), MB_OK);
	return 0;

通常、子ウインドウの通知はWM_COMMANDかWM_NOTIFYとして親ウインドウに通知されますが、 作成と破棄や、一部のクリックはWM_PARENTNOTIFYとして通知されることになっています。 wParamの下位ワードがの場合はウインドウが破棄されようとすることを意味し、 上位ワードにはその対象となるウインドウのIDが格納されています。 よって、上記のif文が成立する場合は、ツールウインドウが破棄されようとしていることを意味します。 WM_PARENTNOTIFYが不要な場合は、子ウインドウの第1引数にWS_EX_NOPARENTNOTIFYを指定できますが、 ダイアログのコントロールは既定でWS_EX_NOPARENTNOTIFYを持ちます。



戻る