EternalWindows
ウインドウ管理 / フルスクリーン

臨場感を重視するゲーム等では、従来のウインドウの概観を用いず、 クライアント領域をスクリーン全体に表示することがよくあります。 このような処理は特別にフルスクリーン化と呼ばれますが、フルスクリーンとは、 そのウインドウのクライアント領域でスクリーン全体を覆うことではありません。 概念的には、スクリーンの解像度を変更したことによって、 クライアント領域がスクリーン全体で見えるようになったということです。 解像度の変更は、コントロールパネルの「デスクトップの表示とテーマ」から 行うことができます。

たとえば、ここで解像度を800×600に設定した場合、 スクリーンが表示できるサイズはその範囲のみとなります。 このため、(800, 600)のクライアント領域をスクリーンに表示することは、 クライアント領域がスクリーン全体に表示されることを意味します。 これにより、クライアント領域のサイズは常に一定でよく、 フルスクリーンを実現するには解像度を変更するだけで済みます。 解像度の変更は、ChangeDisplaySettingsで行うことができます。

LONG ChangeDisplaySettings(
  LPDEVMODE lpDevMode,
  DWORD dwflags
);

lpDevModeは、変更するグラフィックスモードを表すDEVMODE構造体のアドレスです。 グラフィックスモードには解像度のみならず、ビット数や周波数が含まれますが、 今回はフルスクリーンを実現するだけなので、次のメンバのみを扱うことになります。

メンバ 意味
dmPelsWidth ピクセルの幅
dmPelsHeight ピクセルの高さ

dwflagsは、グラフィックスモードをどのように変更するかを示す定数で、 今回はCDS_FULLSCREENを指定することになります。 この定数は、そのグラフィックスモードの変更が一時的なものであることを意味し、 アプリケーションが終了するとグラフィックスモードは元に戻ります。 戻り値は、設定の変更が成功したらDISP_CHANGE_SUCCESSFULが返ります。 その他の戻り値は全てエラーを意味しますが、 その中でも特によく返る値はDISP_CHANGE_BADMODEです。 次のコードを見てください。

DEVMODE devMode;

devMode.dmSize       = sizeof(DEVMODE);
devMode.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
devMode.dmPelsWidth  = 700;
devMode.dmPelsHeight = 600;

ChangeDisplaySettings(&devMode, CDS_FULLSCREEN);

まず、dmSizeにDEVMODE構造体のサイズを代入します。 これは、必須の処理とされています。 dmFieldsは、使用するメンバを表す定数を指定します。 今回は解像度を変更するだけなので、 dmPelsWidth(幅)を使用することを意味するDM_PELSWIDTHとdmPelsHeight(高さ)を 意味するDM_PELSHEIGHTを指定します。 そして、dmPelsWidthとdmPelsHeightに新しい解像度のピクセルを代入するのですが、 ここに指定できる値は、ビデオカードやドライバがサポートしているものに限ります。 多く場合、以下の解像度はサポートされていると思われます。

(640, 480), (800. 600), (1280, 1024), (1400, 1050)

先のコードの(700, 600)という解像度は上記の中に入っていないため、 グラフィックスモードは変更することができず、 DISP_CHANGE_BADMODEが返ることになると思われます。 なお、上記のようにグラフィックスモードを列挙したい場合は、 EnumDisplaySettingsを呼び出すことになります。

今回のプログラムはサポートされている解像度を適切に指定し、解像度を変更します。 プログラムを終了させる際は、Escキーを押してください。

#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("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;
	RECT       rc;
	DEVMODE    devMode;

	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, 640, 480);
	AdjustWindowRectEx(&rc, WS_POPUP, FALSE, WS_EX_TOPMOST);

	hwnd = CreateWindowEx(WS_EX_TOPMOST, szAppName, szAppName, WS_POPUP, 0, 0, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;
	
	devMode.dmSize       = sizeof(DEVMODE);
	devMode.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
	devMode.dmPelsWidth  = 640;
	devMode.dmPelsHeight = 480;

	ChangeDisplaySettings(&devMode, CDS_FULLSCREEN);

	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_ESCAPE)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		return 0;


	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

プログラムをフルスクリーンで動作させるには、 まず、サポートされている解像度のどれを使うかを決定し、 その値とクライアント領域のサイズを同じにしなければなりません。 今回は、解像度を(640, 480)に変更するものとして話を進めます。

SetRect(&rc, 0, 0, 640, 480);
AdjustWindowRectEx(&rc, WS_POPUP, FALSE, WS_EX_TOPMOST);

hwnd = CreateWindowEx(WS_EX_TOPMOST, szAppName, szAppName, WS_POPUP, 0, 0, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hinst, NULL);
if (hwnd == NULL)
	return 0;

AdjustWindowRectExで(640, 480)のクライアント領域を持つウインドウサイズを取得します。 この(640, 480)は、サポートされている解像度であるため、 DEVMODE構造体に指定できます。 WS_POPUPというウインドウスタイルは、 そのウインドウがタイトルバーや枠を持たないウインドウ、 つまり、クライアント領域だけで構成されるウインドウにすることを意味します。 このようなウインドウは、ポップアップウインドウと呼ばれます。 WS_EX_TOPMOSTという拡張ウインドウスタイルは、 そのウインドウが常にZオーダーの先頭に位置することを意味します。 このスタイルを指定しないと、たとえフルスクリーン状態になったとしても、 タスクマネージャのようなアプリケーションが上に被さることができてしまいます。 CreateWindowExで、第5引数(X位置)と第6引数(Y位置)を0にするのは、 スクリーンを確実にこのウインドウのクライアント領域で覆いつくすためです。 たとえば、第5引数を30にした場合、0から30の範囲は他のウインドウが表示されることになります。 ちなみに、WS_POPUPを指定してAdjustWindowRectExを呼び出すと、 そこで返される値はウインドウの概観を考慮していないことから変化することはありません。 つまり、640と480という値を、CreateWindowExの幅と高さに直に指定しても問題ありません。

ウインドウスタイルをWS_POPUPとした場合は、 タイトルバーの閉じるボタンからプログラムを終了できないため、 終了させるための機能を明示的に付け足す必要があります。

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

WM_KEYDOWNのwParamには、押下された仮想キーコードが格納されます。 その値がVK_ESCAPEならEscキーが押下されたということなので、 このキーをプログラムの終了を意味するキーとみなし、WM_CLOSEをポストします。 勿論、Escキーでなく他のキーでも問題ないのですが、 ユーザーから直感的に終了と想像できるキーといえば、Escが代表的だと思われます。

フルスクリーンの解除

プログラムの起動時からフルスクリーンにするというのは、 それ自体は悪いことではないと思われますが、 できれば、任意のタイミングで通常のウインドウの状態に戻せる機能は欲しいものです。 ユーザーは常にフルスクリーンを望んでいるとは限りませんし、 フルスクリーンをどうしても解除してほしいと願う場面もあるでしょう。 たとえば、ユーザーがCtrl+Escキーの押下でスタートメニューを表示した場合、 ユーザーはスタートメニューを操作したいわけですから、 そのようなときに解像度が変更されままでは困ります。 このように、ユーザーの外部操作で解像度を元に戻したい場合は、 WM_KILLFOCUSを捕らえるとよいでしょう。

case WM_KILLFOCUS:
	ChangeDisplaySettings(NULL, 0);
	CloseWindow(hwnd);
	return 0;

このメッセージは、ウインドウがフォーカスを失ったときに送られます。 簡単にいうと、そのウインドウが最小化したり他のウインドウに覆われたりしたときに送られ、 先のようなキーの押下のときでも送られます。 このような場合は、ユーザーは他のウインドウを操作したいため、 ChangeDisplaySettingsを上記のように呼ぶことにより既定の解像度に戻します。 CloseWindowを呼び出して意図的に最小化しようとしているのは、 既定の解像度でのポップアップウインドウの表示が、見苦しいものとなるからです。 このとき、ユーザーはタスクバーからそのウインドウを選択することができますから、 右クリックメニューからシステムメニューが表示される可能性を考慮して、 ウインドウスタイルにWS_SYSMENUを追加しておくとよいでしょう。

hwnd = CreateWindowEx(WS_EX_TOPMOST, szAppName, szAppName, WS_POPUP | WS_SYSMENU, ...);	

さて、ユーザーが最小化したウインドウを元に戻したとなると、 ユーザーはアプリケーションを操作したくなったということですから、 アプリケーションはそのタイミングを捕らえ再び解像度を変更することになります。

case WM_SETFOCUS: {
	DEVMODE devMode;

	devMode.dmSize       = sizeof(DEVMODE);
	devMode.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;
	devMode.dmPelsWidth  = 640;
	devMode.dmPelsHeight = 480;

	ChangeDisplaySettings(&devMode, CDS_FULLSCREEN);
	
	return 0;
}

WM_SETFOCUSは、ウインドウがフォーカスを得たときに送られます。 ここで、ChangeDisplaySettingsを呼び出せば、 ウインドウのサイズが元に戻ると共にフルスクリーンの状態になります。 実は、ここにフルスクリーンするコードを書いた時点で、 WinMainにそのコードを書く必要がなくなっています。 というのも、WinMainで呼び出しているShowWndowはWM_SETFOCUSをセンドするので、 必ず一度はWM_SETFOCUSが処理されることになるからです。



戻る