EternalWindows
ウインドウ管理 / モード切替え

通常のウインドウの状態から任意のタイミングでフルスクリーンに切り替えるようなとき、 ウインドウの状態のことをウインドウモードと呼び、 フルスクリーンの状態のことをフルスクリーンモードと呼びます。 前節で説明したように、これらの切り替えはChangeDisplaySettingsを呼ぶことによって可能ですが、 モードの切り替えというのは単純に解像度を変更するだけではありません。 ウインドウスタイルの変更も必要ですし、ウインドウの位置も変更しなければなりません。 それらの処理を1つの関数に実装すると、恐らく以下のような感じになると思われます。

void ChangeMode(HWND hwnd)
{
	DEVMODE dv;
	
	g_bWindowMode = !g_bWindowMode;
	ShowCursor(g_bWindowMode);

	if (g_bWindowMode) {
		ChangeDisplaySettings(NULL, 0);

		SetWindowLongPtr(hwnd, GWL_STYLE, g_dwWindowStyle);
		SetWindowPos(hwnd, HWND_NOTOPMOST, g_rcWindow.left, g_rcWindow.top, g_rcWindow.right - g_rcWindow.left, g_rcWindow.bottom - g_rcWindow.top, 0);	
	}
	else {
		GetWindowRect(hwnd, &g_rcWindow);

		SetWindowLongPtr(hwnd, GWL_STYLE, WS_POPUP);
		SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, g_cxClient, g_cyClient, 0);
		
		dv.dmSize       = sizeof(DEVMODE);
		dv.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
		dv.dmPelsWidth  = g_cxClient;
		dv.dmPelsHeight = g_cyClient;
		ChangeDisplaySettings(&dv, CDS_FULLSCREEN);
	}
	
	SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
	
	ShowWindow(hwnd, SW_SHOW);
}

まず、グローバルに宣言された現在のモードを表すg_bWindowModeの値を逆転させます。 このとき、値がTRUEならばウインドウモードになるということであり、 FALSEならばフルスクリーンモードにするための処理を行うことになります。 ShowCursorは、カーソルを表示するかどうかを決める関数で、 FALSEを指定した場合はカーソルが非表示になります。 g_bWindowModeがFALSEであるときはフルスクリーンを意味しますから、 このようなときはカーソルを表示する必要がないと解釈し、 ShowCursorを呼ぶようにしています。

if (g_bWindowMode) {
	ChangeDisplaySettings(NULL, 0);

	SetWindowLongPtr(hwnd, GWL_STYLE, g_dwWindowStyle);
	SetWindowPos(hwnd, HWND_NOTOPMOST, g_rcWindow.left, g_rcWindow.top, g_rcWindow.right - g_rcWindow.left, g_rcWindow.bottom - g_rcWindow.top, 0);	
}

g_bWindowModeがTRUEのときは、ウインドウモードにするための処理を行います。 このときは、解像度を元に戻さなければならないので、 ChangeDisplaySettingsの第1引数にNULL、第2引数に0を指定します。 その後、ウインドウスタイルをウインドウモード用に戻すため、 予めグローバルで初期化しておいたスタイルを指定します。 SetWindowPosでは、HWND_NOTOPMOSTでウインドウのZオーダーを先頭にしないよう指定し、 g_rcWindowを基にウインドウの位置をフルスクリーンにする前の位置に戻します。 ChangeDisplaySettingsでフルスクリーンを解除すると、 それに伴ってデスクトップ上のウインドウの位置関係が乱れることがあるようなので、 SetWindowPosはChangeDisplaySettingsの後に呼び出すべきです。

else {
	GetWindowRect(hwnd, &g_rcWindow);

	SetWindowLongPtr(hwnd, GWL_STYLE, WS_POPUP);
	SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, g_cxClient, g_cyClient, 0);
	
	dv.dmSize       = sizeof(DEVMODE);
	dv.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
	dv.dmPelsWidth  = g_cxClient;
	dv.dmPelsHeight = g_cyClient;
	ChangeDisplaySettings(&dv, CDS_FULLSCREEN);
}

g_bWindowModeがFALSEであるときは、フルスクリーンモードにするための処理を行います。 これには、SetWindowLongPtrでウインドウスタイルをWS_POPUPにして、 SetWindowPosでウインドウの位置を(0, 0)に、サイズを解像度と同じ値にすることになります。 この時点でウインドウの位置とサイズはフルスクリーンモード用に変更されていますから、 ウインドウモードに戻すときの考え、予めGetWindowRectでウインドウの位置を記憶しておきます。 ちなみに、フルスクリーンモードのウインドウにGetWindowRectやSWP_NOSIZEを指定した SetWindowPosを呼び出すと、スクリーンのサイズが返るので注意してください。

SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);

このSetWindowPosは、SetWindowLongPtrによるスタイル変更を適用するために存在します。 SWP_FRAMECHANGEDがスタイル変更の適用を意味し、この場合はそれ以外の目的はないため、 ウインドウの位置やサイズ、Zオーダーを変更する必要はありません。

上記したChangeModeは、モード切り替えの考え方としては問題ないのですが、 実際にはこの関数を呼び出してもウインドウは表示されることはありません。 どうやら、SetWindowLongPtrでウインドウスタイルを変更するとウインドウが非表示になるようであり、 SWP_FRAMECHANGEDを指定しているにも関わらず、この現象は回避できません。 よって、ChangeModeの最後に次のコードを付け加えることになります。

ShowWindow(hwnd, SW_SHOW);

これで、非表示となったウインドウが表示されるようになります。

今回のプログラムは、最初にウインドウをウインドウモードで表示し、 スペースキーを押すことによってモードを切り替えることができます。 また、フルスクリーンモードでフォーカスを失ったときは、 ウインドウモードに戻すよう設計しています。

#include <windows.h>

const int   g_cxClient = 640;
const int   g_cyClient = 480;
const DWORD g_dwWindowStyle = WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX;

RECT g_rcWindow = {0};
BOOL g_bWindowMode = TRUE;

void ChangeMode(HWND hwnd);
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;
	RECT       rc;

	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;
	
	SetRect(&rc, 0, 0, g_cxClient, g_cyClient);
	AdjustWindowRectEx(&rc, g_dwWindowStyle, FALSE, 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)
{
	switch (uMsg) {

	case WM_KEYDOWN:
		if (wParam == VK_SPACE)
			ChangeMode(hwnd);
		else if (wParam == VK_ESCAPE)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		else
			;
		return 0;

	case WM_KILLFOCUS:
		if (!g_bWindowMode)
			ChangeMode(hwnd);
		return 0;

	case WM_SYSCOMMAND:
		if (!g_bWindowMode && wParam == SC_SCREENSAVE)
			return 0;
		else
			break;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void ChangeMode(HWND hwnd)
{
	DEVMODE dv;
	
	g_bWindowMode = !g_bWindowMode;
	ShowCursor(g_bWindowMode);

	if (g_bWindowMode) {
		ShowWindow(hwnd, SW_HIDE);
		
		ChangeDisplaySettings(NULL, 0);

		SetWindowLongPtr(hwnd, GWL_STYLE, g_dwWindowStyle);
		SetWindowPos(hwnd, HWND_NOTOPMOST, g_rcWindow.left, g_rcWindow.top, g_rcWindow.right - g_rcWindow.left, g_rcWindow.bottom - g_rcWindow.top, 0);	
	}
	else {
		GetWindowRect(hwnd, &g_rcWindow);

		SetWindowLongPtr(hwnd, GWL_STYLE, WS_POPUP);
		SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, g_cxClient, g_cyClient, 0);
		
		dv.dmSize       = sizeof(DEVMODE);
		dv.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
		dv.dmPelsWidth  = g_cxClient;
		dv.dmPelsHeight = g_cyClient;
		ChangeDisplaySettings(&dv, CDS_FULLSCREEN);
	}
	
	SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
	
	ShowWindow(hwnd, SW_SHOW);
}

グローバルに宣言されている定数の意味は、その名が示す通りです。 これらの値はよく必要となるので、定数として管理したほうが効率的でしょう。 ChangeModeでは、ある1つの処理が追加されています。

if (g_bWindowMode) {
	ShowWindow(hwnd, SW_HIDE);

	SetWindowLongPtr(hwnd, GWL_STYLE, g_dwWindowStyle);
	SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE);	

	ChangeDisplaySettings(NULL, 0);
}

このShowWindowの呼び出しは必須ではないのですが、一応説明しておきます。 実は、ChangeDisplaySettingsで解像度を元に戻した際、 スクリーンに黒いゴミ(ウインドウの跡と思われる)のようなものが残ることがあります。 どうやらこの現象は、ウインドウを明示的に非表示することで回避できるようなので、 念のために呼び出しておいたほうがよいかもしれません。 ちなみに、ShowWindowでウインドウを非表示にすると、 WM_KILLFOCUSがセンドされます。

case WM_KILLFOCUS:
	if (!g_bWindowMode)
		ChangeMode(hwnd);
	return 0;

WM_KILLFOCUSは、ウインドウがフォーカスを失ったときに送られます。 フルスクリーンモードでこのメッセージが送られるというのは、 多くの場合、ユーザーが他のアプリケーションを操作する必要性が生じているはずですから、 ChangeModeを呼び出してウインドウモードに戻すのが良心的であるといえます。 ちなみに、先に示したShowWindowの呼び出しではg_bWindowModeがTRUEであるため、 if文が真となることはありません。

フルスクリーンモードのための特別な処理は、WM_SYSCOMMANDでも行われています。

case WM_SYSCOMMAND:
	if (!g_bWindowMode && wParam == SC_SCREENSAVE)
		return 0;
	else
		break;

WM_SYSCOMMANDのwParamには、発生したシステムコマンドが格納されます。 システムコマンドには、最小化や最大化ボタンの押下などを示すSC_で始まる定数が 用意されており、SC_SCREENSAVEというのはスクリーンセーバーが実行されようと していることを意味しています。 SC_SCREENSAVEに限らず、WM_SYSCOMMANDで0を返した場合は、 その処理を無効にすることを意味するため、SC_SCREENSAVEのみで0を返すことにより、 スクリーンセーバーの起動のみを無効にできます。 一般に、アプリケーションをフルスクリーンモードにするというのは、 そのアプリケーションに集中したいという意思の表れでもありますから、 このような処理を行うことによってスクリーンセーバーの起動を無効にするとよいでしょう。


戻る