ダイアログベースアプリ

ウィンドウアプリは「ウィンドウ」を持つのが基本ですが、ちょっとしたアプリケーションを作る場合、CreateWindow関数でウィンドウを作るよりもダイアログで作ってしまう方が簡単です。

今回は以下の画像のようなシンプルな時計アプリをダイアログで作ってみます。
時計アプリのサンプル

リソースファイル

まずはリソースファイルです。


//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ で生成されたインクルード ファイル。
// Resource.rc で使用
//
#define IDD_DIALOG1                     101
#define IDC_CLOCK                       1001
#define IDC_BUTTON_1224                 1002

/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_DIALOG1 DIALOGEX 0, 0, 105, 50
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "現在時刻"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "終了",IDOK,48,30,50,14
    CTEXT           "スタティック",IDC_CLOCK,6,6,96,24
    PUSHBUTTON      "12h/24h",IDC_BUTTON_1224,6,30,36,14
END

時刻を表示するためのスタティックコントロールと、12時間表記/24時間表記を切り替えるためのボタンを配置しています。
「終了」ボタンを押すとアプリは終了します。

プロパティの調整

スタティックコントロールは標準でIDC_STATICという共通のIDが割り振られます。
これは定義済みシンボルと呼ばれるもので、ユーザー定義のリソースヘッダファイル(resource.h)には定義は記述されません。
(定義はリソーススクリプトの先頭のほうでインクルードされている「winres.h」内にあります)

IDがIDC_STATICのままだとプログラムから文字列を書き換えることが出来ないため、プロパティから独自のものに変更しておきます。
独自のIDを割り振るとリソースヘッダファイルにも定義が追加されます。
ここでは「IDC_CLOCK」というIDにします。

また、テキストを中央寄せにするため「テキストの配置」を「Center」に変更しています。
スタティックコントロールの調整

ダイアログウィンドウはそのままだとデスクトップの左上に表示されるので、「中央揃え」を「True」に設定します。
これでプログラムの起動時にデスクトップの中央に表示されるようになります。
ダイアログの調整

プログラムコード

WinMain関数


#include <windows.h>
#include <strsafe.h>
#include "resource.h"

//プロトタイプ宣言
INT_PTR CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
void SetClockString(HWND);

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
	return 0;
}

WinMain関数の処理は非常にシンプルで、DialogBoxマクロを呼び出してモーダルダイアログを作成するだけです。
親や所有者ウィンドウはないので、第三引数のウィンドウハンドルはNULLを指定します。
このダイアログボックスを閉じるとWinMain関数の終端に達し、プログラムが終了します。

なお、SetClockString関数のプロトタイプ宣言はダイアログプロシージャから呼び出す自作関数です。

ダイアログプロシージャ


#define ID_TIMER1	1
#define BUFFERSIZE 32

enum
{
	time12,
	time24
} time1224;

//ダイアログプロシージャ
INT_PTR CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) {
	case WM_INITDIALOG: //ダイアログの初期化
		SetTimer(hWnd, ID_TIMER1, 1000, NULL);
		SetClockString(hWnd);
		return TRUE;

	case WM_TIMER: //タイマー
		SetClockString(hWnd);
		return TRUE;

	case WM_COMMAND: //コントロールの操作
		switch (LOWORD(wParam))
		{
		case IDC_BUTTON_1224:
			time1224 = time1224 == time12 ? time24 : time12;
			SetClockString(hWnd);
			return TRUE;
		case IDOK:
		case IDCANCEL:
			EndDialog(hWnd, IDOK);
			return TRUE;
		}
		break;
	}
	return FALSE;
}

//時刻表示を書き換える関数
void SetClockString(HWND hWnd)
{
	const WCHAR format12[] =
		L"%04d年 %02d月 %02d日\n"
		L"%s %02d時 %02d分 %02d秒";
	const WCHAR format24[] =
		L"%04d年 %02d月 %02d日\n"
		L"%02d時 %02d分 %02d秒";
	
	WCHAR *ampm;
	WCHAR buf[BUFFERSIZE];
	SYSTEMTIME st;

	GetLocalTime(&st);
	if (time1224 == time12)
	{
		if (st.wHour >= 12)
		{
			st.wHour -= 12;
			ampm = L"午後";
		}
		else
		{
			ampm = L"午前";
		}
		StringCchPrintf(buf, BUFFERSIZE, format12,
			st.wYear, st.wMonth, st.wDay,
			ampm, st.wHour, st.wMinute, st.wSecond);
	}
	else 
	{
		StringCchPrintf(buf, BUFFERSIZE, format24,
			st.wYear, st.wMonth, st.wDay,
			st.wHour, st.wMinute, st.wSecond);
	}
	
	SetDlgItemText(hWnd, IDC_CLOCK, buf);
}

ダイアログプロシージャでは、1秒ごとに時間を取得して反映するためにWM_INITDIALOGメッセージタイマーをセットします。
また、後述する自作関数SetClockStringを実行し、起動直後の時刻を表示しておきます。

WM_TIMERメッセージでは自作関数SetClockStringを実行するだけです。

後は時間表記を切り替えるためのenumとその変数(time1224)を定義し、ボタン(IDC_BUTTON_1224)クリック時に変数を書き換えています。

自作関数SetClockStringは、変数time1224の値をチェックして時間表示形式を切り替えています。
最後にSetDlgItemTextマクロでダイアログに時刻を反映します。

動作の調整

ダイアログベースアプリは上記手順で作れますが、もう少し細かくカスタマイズしてみましょう。

Escキーの無効化

ダイアログは標準でEscキーで閉じることができます。
ダイアログベースアプリの場合はEscキーでアプリが終了してしまいますので、この動作を修正します。

Escキーを押したとき、ダイアログにWM_COMMANDメッセージが送られてきます。
この時のWPARAMの下位ワードにIDCANCELが格納されています。
Escキーでダイアログが閉じられるのはこのためなので、このタイミングでGetKeyState関数でEscキーの状態を取得し、押されていた場合は処理をキャンセルします。


//ダイアログプロシージャ
INT_PTR CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) {
	case WM_COMMAND: //コントロールの操作
		switch (LOWORD(wParam))
		{
		case IDOK:
		case IDCANCEL:
			//Escキーが押されていなければ真
			if (GetKeyState(VK_ESCAPE) >= 0)
				EndDialog(hWnd, IDOK);
			return TRUE;
		}
		break;
	//その他の処理は省略

実は「Ctrl + Break」でもEscキーと同様のメッセージが送られてくるため、そちらの処理も追加しても良いでしょう。
ただこのショートカットを間違えて入力してしまうことはほぼないと思われるので気にしなくても良いです。

Enterキーについて

ダイアログ上にフォーカスを持てるコントロールが何もない場合、Enterキーを押下するとダイアログは閉じられます。
これはEscキーと同じように、ダイアログウィンドウに対してIDOKがワードに送られるためです。
そのため、必要に応じてEnterも同様の手順で無効化しておきます。
(今回の時計アプリにはボタンを配置しているため、必要ではありません)


//ダイアログプロシージャ
INT_PTR CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) {
	case WM_COMMAND: //コントロールの操作
		switch (LOWORD(wParam))
		{
		case IDOK:
			//Enterキーが押されていなければ真
			if (GetKeyState(VK_RETURN) >= 0)
				EndDialog(hWnd, IDOK);
			return TRUE;
		case IDCANCEL:
			//Escキーが押されていなければ真
			if (GetKeyState(VK_ESCAPE) >= 0)
				EndDialog(hWnd, IDOK);
			return TRUE;
		break;
	//その他の処理は省略

ダイアログウィンドウもメニューバーを持つことができます。
実装は簡単で、メニューリソースを作っておき、ダイアログウィンドウのプロパティ「メニュー」から設定するだけです。
ダイアログにメニューを設定


//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ で生成されたインクルード ファイル。
// Resource.rc で使用
//
#define IDD_DIALOG1                     101
#define IDR_MENU1                       102
#define IDC_CLOCK                       1001
#define ID_QUIT                         40001
#define ID_TIMEREP                      40002

/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

IDR_MENU1 MENU
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "終了(&X)",                      ID_QUIT
    END
    MENUITEM "24時間表記(&T)",                  ID_TIMEREP
END

//ダイアログプロシージャ
INT_PTR CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HMENU hMenu;
	MENUITEMINFO mii;

	switch (message) {
	case WM_INITDIALOG: //ダイアログの初期化
		hMenu = GetMenu(hWnd);
		SetTimer(hWnd, ID_TIMER1, 1000, NULL);
		SetClockString(hWnd);
		return TRUE;

	case WM_COMMAND: //コントロールの操作
		switch (LOWORD(wParam))
		{
		case ID_TIMEREP:
			time1224 = time1224 == time12 ? time24 : time12;
			SetClockString(hWnd);
			mii.cbSize = sizeof(MENUITEMINFO);
			mii.fMask = MIIM_STRING;
			mii.dwTypeData = time1224 == time12 ? L"24時間表記(&T)" : L"12時間表記(&T)";
			SetMenuItemInfo(hMenu, ID_TIMEREP, 0, &mii);
			DrawMenuBar(hWnd);
			return TRUE;

		case ID_QUIT:
		case IDOK:
			EndDialog(hWnd, IDOK);
			return TRUE;

		case IDCANCEL:
			if (GetKeyState(VK_ESCAPE) >= 0)
				EndDialog(hWnd, IDOK);
			return TRUE;
		}
		break;
	//その他の処理は省略

時間表記を切り替えるボタンを削除し、メニューに移動してみました。
ダイアログにメニューを設定

アクセラレータの設定

ダイアログでアクセラレータを使用する場合、モードレスダイアログで作成する必要があります。


#include <windows.h>
#include "resource.h"

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	MSG msg;
	BOOL ret;

	//アクセラレータの読み込み
	HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));

	//モードレスダイアログの作成
	HWND hDlg = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			return -1;
		}

		//アクセラレータの適用
		if (!TranslateAccelerator(hDlg, hAccelTable, &msg)
			&& !IsDialogMessage(hDlg, &msg))
		{
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

モードレスダイアログなので、ダイアログの終了はDestroyWindow関数を使用するので注意してください。