EternalWindows
メニュー / アクセラレータキー

これまで作成してきたメニューには、たとえば「ファイル」という項目なら Fという文字が追加されたりなどして、キーボードから項目へのアクセスが可能でした。 このようなとき、このFというキーのことをショートカットキーと呼びますが、 項目へのアクセスにはアクセラレータキーでも行うことができます。 アクセラレータキーとは、Ctrl + A や、 Ctrl + Shift + Bのような 複数のキーの組み合わせで成立するキーのことで、 明示的にメニューを開かず項目へアクセスすることができます。

アクセラレータキーはいわば、特定の組み合わせのキーが同時に押された時に 反応するものですから、まずはその組み合わせを定義しなければなりません。 この定義のことをアクセラレータテーブルと呼び、 CreateAcceleratorTableで作成することになります。

HACCEL CreateAcceleratorTable(
  LPACCEL lpaccl,
  int cEntries
);

lpacclは、アクセラレータテーブルの内容を表すACCEL構造体の配列を指定します。 この配列1つずつの要素にキーの組み合わせを格納することになります。 cEntriesは、ACCEL構造体の配列の要素数を指定します。 戻り値のHACCEL型は、アクセラレータテーブルのハンドルとなります。

ACCEL構造体は、次のように定義されています。

typedef struct tagACCEL {
    BYTE fVirt;
    WORD key;
    WORD cmd;
} ACCEL, *LPACCEL;

fVirtは、アクセラレータキーの修飾子として働かせるキーを示す定数を指定します。 次の表の中から、1つ又は複数を選択することになります。

定数 意味
FALTAltキーを修飾子とする。
FCONTROLCtrlキーを修飾子とする。
FSHIFTShiftキーを修飾子とする。
FVIRTKEY この定数を指定した場合、keyメンバは仮想キーコードと解釈される。

keyは、アクセラレータキーとして働かせるキーを指定します。 cmdは、アクセラレータキーの押下によるWM_COMMANDの生成時に、 そのwParamの下位ワードとする値を指定します。 ここにメニュー項目の識別子を指定すれば、 アクセラレータキーの押下が項目へのアクセスと関連付けられることになります。 ちなみに、このWM_COMMANDのwParamの上位ワードは、 アクセラレータキーによって生成されたことを示すため常に1という値になります。

アクセラレータテーブルを作成したら、それを参照することによって アクセラレータキーの押下をWM_COMMANDに変換しなければなりません。 この作業は、TranslateAcceleratorが行います。

int TranslateAccelerator(
    HWND hWnd,
    HACCEL hAccTable,
    LPMSG lpMsg
);

hWndは、WM_COMMANDを受け取るウインドウのハンドルを指定します。 hAccTableは、アクセラレータテーブルのハンドルを指定します。 lpMsgは、MSG構造体のアドレスを指定します。

TranslateAcceleratorの目的は、特定のキーの組み合わせをアクセラレータキーと見立て、 それをWM_COMMANDに変換することですから、そのようなときには押下されたキーに 他の意味が込められてはいけません。 たとえば、Ctrl + A という組み合わせをアクセラレータキーとして押下した場合、 このAというキーが押されたということがWM_KEYDOWNとしてWindowProcに送られては、 ユーザーは普通にAキーを押してしまったと勘違いするかもしれません。 このようなことを防ぐため、TranslateAcceleratorはメッセージループ内で呼び出し、 もし押されたキーがアクセラレータキーである場合は、 DispatchMessageを呼び出さないようにしなくてはなりません。

while (GetMessage(&msg, NULL, 0, 0) > 0) {
	if (!TranslateAccelerator(hwnd, haccel, &msg)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
}

TranslateAcceleratorが0を返すのは、主として押下されたキーの組み合わせが アクセラレータテーブルに記述されている組み合わせと一致しないときであるため、 そのようなときには通常のメッセージとして処理することになります。 また、TranslateAcceleratorは自身が生成したWM_COMMANDを直接WindowProcに送るため、 この関数が制御を返した時点でWM_COMMANDは処理されていることになります。

アクセラレータテーブルを扱う上での最後の作業は、 CreateAcceleratorTableで作成したアクセラレータテーブルを、 アプリケーションが終了する前にDestroyAcceleratorTableで破棄することです。

BOOL DestroyAcceleratorTable(
  HACCEL hAccel
);

hAccelは、アクセラレータテーブルのハンドルを指定します。

今回のプログラムは、ポップアップメニューの節で作成したプログラムに アクセラレータキーを対応させたものです。

#include <windows.h>

#define ID_FILE  10
#define ID_HELP  20
#define ID_SAVE  30
#define ID_EXIT  40
#define ID_ABOUT 50

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub);
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;
	HACCEL     haccel;
	ACCEL      accel[2];

	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;

	accel[0].key   = 0x53; 
	accel[0].cmd   = ID_SAVE;
	accel[0].fVirt = FCONTROL | FSHIFT | FVIRTKEY;

	accel[1].key   = 0x41; 
	accel[1].cmd   = ID_ABOUT;
	accel[1].fVirt = FALT | FVIRTKEY;

	haccel = CreateAcceleratorTable(accel, 2);

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		if (!TranslateAccelerator(hwnd, haccel, &msg)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	
	DestroyAcceleratorTable(haccel);

	return (int)msg.wParam;
}

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

	case WM_CREATE: {
		HMENU hmenu;
		HMENU hmenuPopupFile;
		HMENU hmenuPopupHelp;

		hmenu  = CreateMenu();
		hmenuPopupFile = CreatePopupMenu();
		hmenuPopupHelp = CreatePopupMenu();

		InitializeMenuItem(hmenuPopupFile, TEXT("保存(&S) Ctrl + Shift + S"), ID_SAVE, NULL);
		InitializeMenuItem(hmenuPopupFile, NULL, 0, NULL);
		InitializeMenuItem(hmenuPopupFile, TEXT("終了(&X)"), ID_EXIT, NULL);
		
		InitializeMenuItem(hmenuPopupHelp, TEXT("バージョン情報(&A) Alt + A"), ID_ABOUT, NULL);

		InitializeMenuItem(hmenu, TEXT("ファイル(&F)"), ID_FILE, hmenuPopupFile);
		InitializeMenuItem(hmenu, TEXT("ヘルプ(&H)"), ID_HELP, hmenuPopupHelp);
		
		SetMenu(hwnd, hmenu);

		return 0;
	}
	
	case WM_COMMAND: {
		int nId = LOWORD(wParam);

		if (nId == ID_SAVE)
			MessageBox(hwnd, TEXT("保存が選択されました。"), TEXT("OK"), MB_OK);
		else if (nId == ID_EXIT)
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		else if (nId == ID_ABOUT)
			MessageBox(hwnd, TEXT("Menu Sample Ver 1.0"), TEXT("バージョン情報"), MB_OK);
		else
			;
		
		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId, HMENU hmenuSub)
{
	MENUITEMINFO mii;
	
	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask  = MIIM_ID | MIIM_TYPE;
	mii.wID    = nId;

	if (lpszItemName != NULL) {
		mii.fType      = MFT_STRING;
		mii.dwTypeData = lpszItemName;
	}
	else
		mii.fType = MFT_SEPARATOR;

	if (hmenuSub != NULL) {
		mii.fMask   |= MIIM_SUBMENU;
		mii.hSubMenu = hmenuSub;
	}

	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

ポップアップメニューの項目を見れば分かるように、 Ctrl + Shift + S というようにアクセラレータキーへの対応を示しています。 勿論、項目の名前をこのように加工したからといって、 そのキーの押下がアクセラレータキーとして働くわけではなく、 きちんとアクセラレータテーブルを作成しなければなりません。 次のコードは、WinMainの一部です。

accel[0].key   = 0x53; 
accel[0].cmd   = ID_SAVE;
accel[0].fVirt = FCONTROL | FSHIFT | FVIRTKEY;

accel[1].key   = 0x41; 
accel[1].cmd   = ID_ABOUT;
accel[1].fVirt = FALT | FVIRTKEY;

haccel = CreateAcceleratorTable(accel, 2);

今回のプログラムは、アクセラレータキーに対応する項目は2つしているので、 ACCEL型の変数のaccelの要素数は2となります。 まず、「保存」という項目とアクセラレータキーを関連付けるため、 cmdにはその項目の識別子であるID_SAVEを指定します。 fVirtは、アクセラレータキーの修飾子と働かせるキーであり、 「保存」という項目はCtrl + Shiftという修飾子を持たせる設計にしたいため、 FCONTROLとFSHIFTを組み合わせて指定することになります。 FVIRTKEYは、keyメンバが仮想キーコードであることを示すもので、ここでは0x53としています。 この0x53という値は、仮想キーコードのSという文字に相当します。 仮想キーコードの一覧はここでは示しませんが、 0x41から0x5AまでがAからZに対応しているということは覚えておいてください。


戻る