アクセラレータ

メニュー項目の実行は「Alt + 文字キー」で可能ですが(アクセスキー)、さらに自由度が高く使い勝手が良い方法にキーボードアクセラレータ(アクセラレータキーホットキー)というものがあります。
これはいわゆる「ショートカットキー(キーボードショートカット)」の機能で、任意のキーの組み合わせで即座に任意の機能を実行することができます。

アクセラレータの作成

アクセラレータの作成の前に、まずメニューを作成します。
メニューバーの項を参考にして適当なメニューを用意してください。
ここでは以下の内容のメニューを使用します。


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

IDR_MENU1 MENU
BEGIN
    POPUP "ファイル"
    BEGIN
        MENUITEM "保存",                          ID_40001
        MENUITEM SEPARATOR
        MENUITEM "終了",                          ID_40002
    END
END

アクセラレータはリソーススクリプトに記述します。
リソースの追加ダイアログで「Accelerator」を選択し「新規作成」を押下します。
アクセラレータの新規作成

すると以下のような画面が開きます。
これはアクセラレータテーブルを編集するエディタです。
(テーブル=表)
アクセラレータエディタ

アクセラレータは「ID」「修飾子」「キー」「タイプ」の4つの設定項目があります。

ID列は現在のプログラム上で定義されている識別子がドロップダウンリスト形式で表示されます。
プログラマが定義したもののほか、システム定義済みのものも多数表示されます。
ここで指定した識別子の項目がキーボードショートカットで起動できるようになります。
キーボードから直接入力することで新しくIDを作成することもできます。

修飾子列は修飾キーの指定です。
「Ctrl」「Shift」「Alt」キーとの組み合わせを指定できます。
後述する「タイプ」列がASCIIの場合、Altキーのみここで指定できます。

キー列はショートカットに使用する実際のキーです。
ドロップダウンリストから仮想キーコードを選択できるほか、「A」や「B」などの文字を直接入力することができます。
「タイプ」列がASCIIの場合、Ctrlキーは「^」記号で表現します。
例えば「^a」は「Ctrl + A」を意味します。
Shiftキーは大文字で指定します。
例えば「^A」は「Ctrl + Shift + A」を意味します。
Altキーは「修飾子」列で指定します。
「タイプ」列がVIRTKEY(仮想キー)の場合は修飾キーは全て「修飾子」列で指定します。
(入力文字は自動で大文字になります)

タイプ列はキーの指定方法を仮想キーにするかASCII文字で指定するかを変更します。
「VIRTKEY」が仮想キー、「ASCII」がASCII文字での指定です。
ASCIIを指定すると、キーの大文字と小文字が区別されるようになります。
CapsLockの影響を受けるようになり、ややこしいのでお勧めしません。

ここでは以下のように設定してみます。
アクセラレータの設定例

リソースファイルは以下のようになります。


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

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

IDR_MENU1 MENU
BEGIN
    POPUP "ファイル"
    BEGIN
        MENUITEM "保存",                          ID_40001
        MENUITEM SEPARATOR
        MENUITEM "終了",                          ID_40002
    END
END


/////////////////////////////////////////////////////////////////////////////
//
// Accelerator
//

IDR_ACCELERATOR1 ACCELERATORS
BEGIN
    "S",            ID_40001,               VIRTKEY, CONTROL, NOINVERT
    "Q",            ID_40002,               VIRTKEY, SHIFT, CONTROL, NOINVERT
END

アクセラレータにNOINVERTというキーワードが自動で付けられますが、これは16ビット時代のWindowsとの互換性のために残されているものです。
(無視して構いません)

アクセラレータの適用

WinMain関数

作成したアクセラレータはLoadAccelerators関数で読み込むことができます。

HACCEL LoadAcceleratorsW(
 HINSTANCE hInstance,
 LPCWSTR lpTableName
);
モジュールhInstanceに含まれるアクセラレータテーブルlpTableNameを読み込む。
失敗した場合はNULLを返す。

アクセラレータテーブルハンドルはHACCEL型です。
lpTableNameはアクセラレータテーブル名、またはMAKEINTRESOURCEマクロで識別子を指定します。

MAKEINTRESOURCEマクロを使用する理由についてはリソースを指定する識別子および文字列についてを参照してください。

読み込んだアクセラレータテーブルハンドルはTranslateAccelerator関数を使用して、ウィンドウプロシージャにメッセージを送信します。

int TranslateAcceleratorW(
 HWND hWnd,
 HACCEL hAccTable,
 LPMSG lpMsg
);
アクセラレータテーブルhAccTableを使用してキー入力を変換し、ウィンドウhWndのウィンドウプロシージャにメッセージlpMsgを送信する。
成功した場合は0以外を、失敗した場合は0を返す。

TranslateAccelerator関数は、キー入力(WM_KEYDOWNWM_SYSKEYDOWN)を監視し、アクセラレータテーブルで定義したキーの組み合わせと一致するキー入力があった場合に、キー入力をWM_COMMANDまたはWM_SYSKEYDOWNに変換してウィンドウプロシージャに送信します。
キー入力以外のメッセージや、一致するキーが無かった場合は0を返します。

これらの関数はWinMain関数内で実行します。
具体的には以下のように使用します。


int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	MyRegisterClass(hInstance);

	if (!InitInstance(hInstance, nCmdShow))
	{
		return 0;
	}

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

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			return -1;
		}
		//アクセラレータによる変換と送信
		if(!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{//アクセラレータで処理されなかった
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

TranslateAccelerator関数の戻り値を反転(!演算子)している点に注意してください。
TranslateAccelerator関数が成功した場合、この関数がウィンドウプロシージャへメッセージを送信してくれるので、TranslateMessage関数およびDispatchMessage関数は実行しません。

ウィンドウプロシージャ

ウィンドウプロシージャは以下のように定義しておきます。
(サンプルなので適当です)


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_COMMAND: //コントロールの操作
		switch (LOWORD(wParam))
		{
		case ID_40001:
			MessageBox(hWnd, L"「保存」を実行しました。", L"情報", MB_OK);
			break;
		case ID_40002:
			MessageBox(hWnd, L"「終了」を実行しました。", L"情報", MB_OK);
			break;
		}
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

これはメニューバーを設置する場合のコードと全く同じです。
TranslateAccelerator関数がキー入力をWM_COMMANDメッセージに変換してくれるので、WM_KEYDOWNメッセージの処理などを追加することなくキーボードショートカットの機能が実現できます。

アクセラレータの使用はメニューの設置が必須というわけではありません。
あくまでも特定のWM_KEYDOWNメッセージをWM_COMMANDメッセージに変換する処理を行うに過ぎないので、メニューが無いウィンドウでも問題なく動作します。
ただし、メニュー項目と共通のショートカットを作成する場合、そのメニュー項目が選択不可状態になっている場合はショートカットキーも無効になります。

実行するメニュー項目がシステムメニュー内にある場合、WM_SYSCOMMANDメッセージが送信されます。

メニューの項目をショートカットキーで実行出来る場合、その対応するキーを項目に表示しておくのが一般的です。
なので、リソーススクリプトを以下のように編集しておきましょう。


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

IDR_MENU1 MENU
BEGIN
    POPUP "ファイル"
    BEGIN
        MENUITEM "保存\tCtrl + S",				ID_40001
        MENUITEM SEPARATOR
        MENUITEM "終了\tCtrl + Shift + Q",		ID_40002
    END
END

「\t」はタブ文字を表します。
メニュー項目にショートカットキーを表示

システムアクセラレータ

Windowsは標準でシステムアクセラレータを持っており、ウィンドウアプリは最初からいくつかのショートカットキーを備えています。
これらのショートカットキーと同じキーをアクセラレータに設定すると動作が上書きされます。
ショートカットキーの上書きはWindowsで共通のショートカットキーが使えなくなり、ユーザーを混乱させる可能性があるので推奨されません。

システムアクセラレータには以下があります。

キー 説明
Alt+Esc 次のアプリケーションに切り替え
Alt+F4 アプリケーションまたはウィンドウを閉じる
Alt+ハイフン ドキュメントウィンドウのウィンドウメニューを開く
Alt+PrintScreen アクティブウィンドウのスクリーンショットをクリップボードにコピーする
Alt+Space アプリケーションのウィンドウメニューを開く
Alt+Tab 次のアプリケーションに切り替え
Shift+Alt+Tab 前のアプリケーションに切り替え
Ctrl+Esc スタートメニューを開く
Ctrl+F4 アクティブなグループまたはドキュメントウィンドウを閉じる
F1 アプリケーションのヘルプを開く(ある場合)
PrintScreen 画面全体のスクリーンショットをクリップボードにコピー

ウィンドウ上にコントロール類がある場合

キー入力のメッセージ(WM_KEYDOWNなど)は現在フォーカスを持っているウィンドウに対して送信されます。
ウィンドウ上にコントロール類を配置していてそれらがフォーカスを持っている場合、キーボードメッセージはコントロールのウィンドウプロシージャに送られます。

ボタンなどのコントロールもウィンドウの一種で、親ウィンドウ上に配置される子ウィンドウです。
これらも独自のウィンドウプロシージャを持っています。
コントロールが受信したキーボードメッセージは親ウィンドウに送信されません。

アクセラレータは通常は親ウィンドウ(メインのウィンドウ)のウィンドウプロシージャで処理します。
上記のコードではコントロールがフォーカスを持っている場合にアクセラレータが反応しないので、少し変更する必要があります。


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

//グローバル変数
HINSTANCE hInst;
WCHAR szTitle[] = L"テストアプリ";
WCHAR szWindowClass[] = L"MyTestApp";

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
HWND InitInstance(HINSTANCE, int); //戻り値をHWND型に変更
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	MyRegisterClass(hInstance);

	//メインウィンドウのハンドルを取得
	HWND hWnd = InitInstance(hInstance, nCmdShow);
	if (!hWnd)
	{
		return 0;
	}

	HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			return -1;
		}
		//アクセラレータによる変換と送信
		//メインウィンドウのハンドルを渡す
		if (!TranslateAccelerator(hWnd, hAccelTable, &msg))
		{//アクセラレータで処理されなかった
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		//修正前の処理
		//if(!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		//{
		//	TranslateMessage(&msg);
		//	DispatchMessage(&msg);
		//}
	}

	return (int)msg.wParam;
}

//ウィンドウの作成
//HWND型を返す
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	hInst = hInstance;

	HWND hWnd = CreateWindow(
		szWindowClass, szTitle,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	if (!hWnd) {
		return NULL;
	}
	ShowWindow(hWnd, nCmdShow);

	return hWnd;
}

//残りは省略

最初のコードでは、TranslateAccelerator関数に渡すウィンドウハンドル(第一引数)はmsg.hwndを渡していました。
msg.hwndにはメッセージが発生したウィンドウのハンドルが格納されています。
コントロールでメッセージが発生した場合はコントロールのハンドルが格納されているのですが、これをTranslateAccelerator関数に渡してもアクセラレータは効きません。
これをあらかじめ保存しておいたメインウィンドウのハンドルに変更することでアクセラレータが有効になります。