キー入力

キーボードの押下

マウス操作と同じく、キーボードのキー入力も「押す」と「離す」の二種類のメッセージが通知されます。

メッセージ 説明
WM_KEYDOWN キーを押す
WM_KEYUP キーを離す
WM_SYSKEYDOWN システムキーを押す
WM_SYSKEYUP システムキーを離す

WM_KEYDOWN/WM_KEYUPメッセージはAltキーとF10キー以外のキー入力時に通知されます。
AltキーとF10キーはシステムキーといって、Windowsの操作のための特殊なキーなので別扱いになっています。
(それ以外のキーを一般キーともいいます)

Windows自体はマウスが無くても、キーボードのみで操作可能なように作られています。
(Windows上で動作するアプリケーションはその実装次第)
AltキーとF10キーはウィンドウのメニュー項目を選択するために使用されています。

キーボードはキーを押しっぱなしにすると連続して文字が入力されますが、その都度WM_KEYDOWNメッセージが通知されます。

キーの判別

キーボードメッセージが送られてきたとき、ウィンドウプロシージャのWPARAMには入力があったキーの仮想キーコード(Virtual Key Codes)というものが格納されています。
仮想キーコードはキーボードの機種による違いを吸収するもので、機種によって異なる「生の」キー入力情報を仮想的なキー情報に変換し、汎用的に扱うための仕組みです。

仮想キーコードは「VK_○○」という形式で定数が用意されています。
ただし英数字のキーなど、機能ではなく文字が割り当てられているキーに関してはASCIIコードと同じなので定数はありません。

仮想キーコード定数はかなり数が多いので主要なものを紹介します。

仮想キーコード 説明 16進数
VK_BACK BackSpace 0x08
VK_TAB Tab 0x09
VK_CLEAR Clear 0x0C
VK_RETURN Return(Enter) 0x0D
VK_SHIFT Shift 0x10
VK_CONTROL Ctrl 0x11
VK_MENU Alt 0x12
VK_PAUSE Pause 0x13
VK_CAPITAL Caps Lock 0x14
VK_ESCAPE Escape 0x1B
VK_SPACE Space 0x20
VK_PRIOR Page Up 0x21
VK_NEXT Page Down 0x22
VK_END End 0x23
VK_HOME Home 0x24
VK_LEFT 0x25
VK_UP 0x26
VK_RIGHT 0x27
VK_DOWN 0x28
VK_PRINT Print Screen 0x2A
VK_INSERT Insert 0x2D
VK_DELETE Delete 0x2E
VK_LWIN 左Windows 0x5B
VK_RWIN 右Windows 0x5C
VK_APPS Menu
(アプリケーションキー)
0x5D
VK_NUMPAD0~
VK_NUMPAD9
テンキー0~9 0x60~
0x69
VK_MULTIPLY テンキー* 0x6A
VK_ADD テンキー+ 0x6B
VK_SEPARATOR テンキーEnter 0x6C
VK_DECIMAL テンキー. 0x6E
VK_DIVIDE テンキー/ 0x6F
VK_F1~
VK_F12
F1~F12 0x70~
0x7B
VK_F13~
VK_F24
F13~F24 0x7C~
0x87
VK_NUMLOCK Num Lock 0x90
VK_SCROLL Scroll Lock 0x91

キーボードの機種によっては搭載していないキーがあったり、別のキーなのに同じ仮想キーコードが送られてくることもあります。
例えばReturnキーとテンキーのEnterは同じ仮想キーとなることが多いです。

Shiftキー、Ctrlキー、Altキーなどは左右にあることが多いですが、左右どちらのキーから入力されたかはWPARAMを調べるだけでは判別することはできません。

仮想キーコードを表示するサンプルコードです。


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

//ウィンドウの生成等は省略

#define BUFFERSIZE 128

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	RECT rt;

	static int c;

	WCHAR buf[BUFFERSIZE];
	static const WCHAR* format = L"キーコード: 0x%02X";

	switch (message)
	{
	case WM_PAINT: //ウィンドウの描画
		StringCchPrintf(buf, BUFFERSIZE, format, c);
		GetClientRect(hWnd, &rt);
		hdc = BeginPaint(hWnd, &ps);

		DrawText(hdc, buf, -1, &rt, DT_WORDBREAK | DT_EXPANDTABS);

		EndPaint(hWnd, &ps);
		break;

	case WM_KEYDOWN: //キー押下
		c = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

	case WM_SYSKEYDOWN: //システムキー押下
		c = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

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

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

以下は「A」キーを押したところです。
キーダウンの検出

「VK_A」という仮想キーコードの定数はありませんが、char型の「'A'」は内部的には「0x41(10進数で65)」なので、定数は必要ないわけです。

文字の判別

WM_KEYDOWNメッセージで取得できる仮想キーコードは、「押したキーの種類」を取得するもので、実際に入力される文字の判別には使えません。
大文字の「A」と小文字の「a」では同じ番号が取得されるためです。

ShiftキーやCaps Lockなどの状態をすべて調べて文字を判別するようなコードを書くことも出来なくはないかもしれませんが、もっと簡単に判別する方法が用意されています。

WinMain関数内のメッセージループでは、GetMessage関数を使用してウィンドウに通知されたメッセージを取得していました。
このメッセージ(MSG構造体の変数)はDispatchMessage関数でウィンドウプロシージャに送られるわけですが、その手前でTranslateMessage関数を呼び出します。

BOOL TranslateMessage(
 const MSG *lpMsg
);
仮想キーコードを文字メッセージに変換する。
キー入力メッセージを処理した場合は0以外を返す。
それ以外の場合は0を返す。

つまりメッセージループは以下のようになります。


MSG msg;
BOOL ret;

//メッセージループ
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
	if (ret == -1)
	{
		return -1;
	}
	else
	{
		TranslateMessage(&msg); //これを追加
		DispatchMessage(&msg);
	}
}

このようにすると、文字キーの入力が発生したときにWM_CHARというメッセージが発行されます。
システムキーの入力の場合はWM_SYSCHARメッセージが発行されます。
ウィンドウプロシージャ内ではこのメッセージを補足して処理を行います。

WM_CHARメッセージが通知されたとき、WPARAMには文字コードが入っています。
これは大文字小文字の状態を適切に処理した後の値なので、そのまま文字として表示することができます。


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

//グローバル変数
HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

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

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

	MSG msg;
	BOOL ret;

	//メッセージループ
	while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
	{
		if (ret == -1)
		{
			//GetMessage関数の実行失敗
			return -1;
		}
		else
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	//省略
}

//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	//省略
}

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	RECT rt;

	static int v, c;

	WCHAR buf[BUFFERSIZE];
	static const WCHAR* format = L"仮想キー:\t0x%02X\n"
		L"文字コード:\t0x%02X\n"
		L"文字:\t\t%c";

	switch (message)
	{
	case WM_PAINT: //ウィンドウの描画
		StringCchPrintf(buf, BUFFERSIZE, format, v, c, c);
		GetClientRect(hWnd, &rt);
		hdc = BeginPaint(hWnd, &ps);

		DrawText(hdc, buf, -1, &rt, DT_WORDBREAK | DT_EXPANDTABS);

		EndPaint(hWnd, &ps);
		break;

	case WM_KEYDOWN:
		v = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

	case WM_SYSKEYDOWN:
		v = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

	case WM_CHAR:
		c = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

	case WM_SYSCHAR:
		c = wParam;
		InvalidateRect(hWnd, NULL, TRUE);
		break;

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

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

このコードは入力されたキーの仮想キーコード、文字コード、文字を表示します。
文字の種類の判別

キー入力メッセージが送られてくる順番は

  1. WM_KEYDOWN
  2. WM_CHAR
  3. WM_KEYUP

となります。

GetKeyState関数

上記のキーの判別は、そのキーが押下された(または離された)時に、そのメッセージの処理内でのみ可能です。
それ以外の個所で特定のキーの状態を知るにはGetKeyState関数を使用します。

SHORT GetKeyState(
 int nVirtKey
);
キーnVirtKeyの状態を取得する。

引数は状態を取得したいキーの仮想キーコードです。
(文字キーの場合はASCIIコード)

戻り値は少しややこしいです。
最上位ビットが1の場合、キーが押されています。
0ならば押されていません。
最下位ビットはキーを押すごとに1と0の状態が切り替わります。
これはCapsLockキーの状態を知る場合などに利用します。

キーが押されているか否かは戻り値の最上位ビットを調べれば良いわけです。
最上位ビットが0なら正数、1なら負数になりますから、0未満か否かを判定することでキーの状態を判定することができます。
(律儀に「0x8000」とビットAND演算しても良いです→ビット演算の活用法)

GetKeyState関数は例えば修飾キーと文字キーが同時に押されているか否かを判別する時に使用できます。


case WM_KEYDOWN:
	//「Ctrl + A」の入力を判定する
	if (wParam == 'A' && GetKeyState(VK_CONTROL) < 0) {
		MessageBox(hWnd, L"Ctrl + Aが押されました。", L"情報", MB_OK);
	}
	break;

「A」キーが押された時に送られてくるWM_KEYDOWNメッセージでは、WPARAMには「'A'」の情報しか入っていません。
GetKeyState関数を使用することで、同時押しを判別できるようになります。

なお、GetKeyState関数では以下の仮想キーコードが追加で使用できます。

仮想キーコード 説明 16進数
VK_LSHIFT 左Shift 0xA0
VK_RSHIFT 右Shift 0xA1
VK_LCONTROL 左Ctrl 0xA2
VK_RCONTROL 右Ctrl 0xA3
VK_LMENU 左Alt 0xA4
VK_RMENU 右Alt 0xA5

キーボードメッセージが送られてこない?

ウィンドウ上にボタンなどのコントロール類を配置した場合、WM_KEYDOWNなどのキーボードメッセージがウィンドウに送られてこないことがあります。
これはコントロール類にフォーカスがあるとキーボードメッセージはそのコントロールのウィンドウプロシージャに送られるのが原因です。
(コントロールは親ウィンドウにメッセージを再送信しない)

これの解決法は、何らかの方法でフォーカスを親ウィンドウにセットしなおす、コントロールをサブクラス化して親ウィンドウにメッセージを再送する、などの方法があります。