GetKeyStateとGetAsyncKeyStateの違い:ビットの意味と代替手段
GetKeyState・GetAsyncKeyState の最上位・最下位ビットをドキュメントどおりに整理し、キュー上の状態と物理状態の違い、ポーリングより WM_KEYDOWN・Raw Input を使う理由を解説。
2026年11月改訂: この記事は2021年に韓国語で公開したものを書き直しました。C++ のサンプル2本は MinGW-w64 14.0.0(GCC 16.2.0)で Windows 向けにコンパイルしましたが、この記事を書いた Mac では実行していません。 実行画面の代わりに、実際のコンパイル結果と Microsoft Learn のドキュメントを根拠にしています。2021年版の GetAsyncKeyState の戻り値の表は最下位ビットの意味が逆で、GetKeyState の最下位ビットを「以前に押したことがある」とした説明も誤りでした(実際はトグル状態)。
この記事は、Win32 でキーが押されているかを調べたいが、GetKeyState と GetAsyncKeyState のどちらを使うべきか、& 0x8000 が何を確かめているのか迷っている方向けです。結論は、信頼してよいのは最上位ビットだけ(< 0 または & 0x8000)。キーメッセージの処理中なら GetKeyState、今この瞬間の物理状態が必要なら GetAsyncKeyState です。そして多くの場合、ループで問い合わせ続けるより WM_KEYDOWN や Raw Input でイベントを受け取るほうが適切です。
ドキュメントが定める2つのビットと、2021年版の表の訂正
SHORT WINAPI GetAsyncKeyState(int vKey);
SHORT WINAPI GetKeyState(int nVirtKey);
どちらも 16 ビットの SHORT を返し、ドキュメントが意味を定めているビットは2つだけです。
| ビット | GetAsyncKeyState | GetKeyState |
|---|---|---|
| 最上位 0x8000 | 呼び出した瞬間にキーが押されている | このスレッドが最後に読んだメッセージの時点で押されている |
| 最下位 0x0001 | 前回の GetAsyncKeyState 呼び出し以降に押された(頼ってはいけない) | トグル状態(Caps Lock などがオン) |
2021年版の表は 0x8000 を「以前に押したことがなく、今押されている」、0x8001 を「以前に押したことがあり、今も押されている」としていました。ドキュメントに照らすと逆です。最下位ビットが 1 なのは前回の呼び出し以降に新たに押されたということなので、押した最初の瞬間に見えるのが 0x8001、押しっぱなしで新たな押下がなければ 0x8000 です。同じ記事の本文にあった「最初に入力されたときは 0x8001」という一文のほうが正しく、表と矛盾していました。
さらに大事なのは、この最下位ビットを使わないことです。Microsoft Learn の GetAsyncKeyState の解説によると、このビットは 16 ビット Windows との互換のために残されているだけで、プリエンプティブなマルチタスク環境では、別のアプリが先に GetAsyncKeyState を呼んで「押された」ビットを持っていってしまうことがあります。2021年のサンプルコードが & 0x8000 で最上位ビットだけを見ていたのは、正しい習慣でした。
GetKeyState について2021年版は「押されていれば負の値(0xffffff80、0xffffff81)を返す」としていました。これは 16 ビットの値を int に広げて %x で表示したときの見え方です。Windows に依存しない部分なので、Mac 上の clang で確かめました。
#include <cstdio>
#include <cstdint>
int main()
{
const int16_t samples[] = {0x0000, 0x0001, (int16_t)0x8000, (int16_t)0x8001,
(int16_t)0xFF80, (int16_t)0xFF81};
for (int16_t s : samples)
std::printf("%6d %%x=%-9x down=%d low=%d\n",
s, s, (s & 0x8000) != 0, s & 1);
}
0 %x=0 down=0 low=0
1 %x=1 down=0 low=1
-32768 %x=ffff8000 down=1 low=0
-32767 %x=ffff8001 down=1 low=1
-128 %x=ffffff80 down=1 low=0
-127 %x=ffffff81 down=1 low=1
(Apple clang 21.0.0、-std=c++20 -Wall -Wextra、警告なし。)0xFF80 でも 0x8000 でも、最上位ビットが立っていれば SHORT としては負なので、< 0 と & 0x8000 は常に同じ答えになります。ほかのビットには決まった意味がないので、== 0x8000 のような比較は書いてはいけません。また2021年版の「以前に押したことがあり、今押していなければ 1 のまま残る」という説明も誤りです。GetKeyState の最下位ビットはトグル状態で、意味を持つのは Caps Lock・Num Lock・Scroll Lock のようなキーです。
メッセージキュー上の状態と物理状態
2つの関数の本当の違いは「どの時点の状態を読むか」です。2021年版の「GetKeyState はメッセージキューに残ったメッセージによって戻り値が変わる」という説明は方向としては合っていますが、正確には次のとおりです。
- GetAsyncKeyState:システムがハードウェア入力を受け取るとすぐ更新する非同期キー状態を読みます。呼び出した瞬間の物理状態です。
- GetKeyState:呼び出したスレッドのキー状態を読みます。この状態は、スレッドが
GetMessage/PeekMessageでキーボードメッセージを取り出したときに進みます。ドキュメントの言葉では「ハードウェアの割り込みレベルの状態は反映しない」です。
だからこそ GetKeyState が向いている場面があります。ユーザーが Ctrl+S を素早く押してすぐ離し、プログラムが少し忙しくて WM_KEYDOWN('S') の処理が遅れたとします。その時点で GetAsyncKeyState(VK_CONTROL) は「押されていない」と答えますが、GetKeyState(VK_CONTROL) は処理中のメッセージが作られたときの状態を返すので、正しく Ctrl+S として扱えます。キーメッセージの中で修飾キーを確認するなら GetKeyState です。
この違いから、2021年版の2本目のサンプル(コンソールで GetKeyState を使って @ を動かすもの)は、ドキュメント上は動作が保証されません。コンソールプログラムのスレッドはウィンドウを持たずキーボードメッセージを受け取らないため、スレッドのキー状態が進むきっかけがないからです。2021年には動いたと書いていますが、今回は実行できなかったので結果は断定しません。コンソールでキー入力を扱うなら、ReadConsoleInput でキーイベントを読むのがドキュメントに沿った方法です。
2021年のサンプルの再コンパイルと、ポーリングの問題点
2021年の2本(GetAsyncKeyState 版と GetKeyState 版)は、-Wall -Wextra でエラーも警告もなくコンパイルできました。ただ、コンパイラが黙っていても設計まで良いとは限りません。ループはこういう形でした。
while (true) {
gotoxy(x, y);
putch('@');
if (GetAsyncKeyState(VK_UP) & 0x8000) y--;
// … DOWN, LEFT, RIGHT
Sleep(50);
}
この方式の問題点です。
- 短い入力を取りこぼします。 50ms ごとに「今押されているか」しか見ないので、2回の確認の間に押して離した入力は見えません。これを補おうと最下位ビットを使うと、前述のとおり他のアプリと奪い合いになります。
- フォーカスを無視します。
GetAsyncKeyStateはシステム全体の状態なので、ユーザーが別のウィンドウで矢印キーを押しても@が動きます。キーボード全体を監視する形なので、セキュリティソフトに怪しまれる可能性もあります。 - 何もなくても起きます。 入力がなくても毎秒20回起き、反応は最大 50ms 遅れます。
- 座標の範囲チェックがありません。
x・yが負になるとSetConsoleCursorPositionは失敗しますが、戻り値を見ていません。
毎フレーム描画するゲームループなら、フレームごとにキー状態を読むこと自体はよくある方法です。それでも、その状態を WM_KEYDOWN/WM_KEYUP や Raw Input のイベントから自分で管理すれば、1 と 2 の問題は避けられます。
2026年版:WM_KEYDOWN と Raw Input
// x86_64-w64-mingw32-g++ -municode -mwindows -Wall -Wextra keys2026.cpp -o keys2026.exe -luser32 -lgdi32
#include <windows.h>
static int g_x = 10, g_y = 10;
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_KEYDOWN: {
bool repeat = (lp & (1L << 30)) != 0; // ビット30:すでに押されていた(オートリピート)
bool ctrl = GetKeyState(VK_CONTROL) < 0; // このメッセージ時点の Ctrl
int step = ctrl ? 10 : 1;
if (repeat && wp == VK_SPACE) return 0; // Space はリピートを無視
switch (wp) {
case VK_UP: g_y -= step; break;
case VK_DOWN: g_y += step; break;
case VK_LEFT: g_x -= step; break;
case VK_RIGHT: g_x += step; break;
default: return 0;
}
InvalidateRect(hwnd, nullptr, TRUE);
return 0;
}
case WM_INPUT: { // Raw Input:デバイス単位のキーイベント
RAWINPUT ri; UINT size = sizeof(ri);
if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, &ri, &size, sizeof(RAWINPUTHEADER)) != (UINT)-1
&& ri.header.dwType == RIM_TYPEKEYBOARD) {
// ri.data.keyboard.VKey / MakeCode / Flags(RI_KEY_BREAK = 離した)
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC dc = BeginPaint(hwnd, &ps);
TextOutW(dc, g_x, g_y, L"@", 1);
EndPaint(hwnd, &ps);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR, int show)
{
WNDCLASSW wc{};
wc.lpfnWndProc = WndProc;
wc.hInstance = inst;
wc.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = L"KeyDemo";
RegisterClassW(&wc);
HWND hwnd = CreateWindowExW(0, L"KeyDemo", L"Arrow keys (Ctrl = x10)", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 480, 320, nullptr, nullptr, inst, nullptr);
RAWINPUTDEVICE rid{0x01, 0x06, 0, hwnd}; // usage page 1, usage 6 = キーボード
RegisterRawInputDevices(&rid, 1, sizeof(rid));
ShowWindow(hwnd, show);
MSG m;
while (GetMessageW(&m, nullptr, 0, 0) > 0) { // 入力がなければ眠って待つ
TranslateMessage(&m);
DispatchMessageW(&m);
}
return (int)m.wParam;
}
-Wall -Wextra で警告なしにコンパイルでき、生成物は PE32+ executable (GUI) x86-64 でした。実行はしていません。 wWinMain を使うので -municode が必要で、付け忘れるとリンカーが実際に undefined reference to 'WinMain' で止まります。
- WM_KEYDOWN はキーボードフォーカスを持つウィンドウにだけ届きます。
lParamのビット30は直前のキー状態で、オートリピートかどうかが分かります。キーではなく入力された文字が欲しいなら、キー配列や IME(日本語入力)が反映されたWM_CHARを処理します。 - Raw Input はキーボードデバイスからのイベントを
WM_INPUTで受け取ります。複数のキーボードを区別したいときや、ゲームでスキャンコードが必要なときに使います。ウィンドウが背面にあるときも受け取るにはRIDEV_INPUTSINKを明示的に指定する必要があり、その意図がコードに表れます。
C# から呼ぶ場合は LibraryImport で宣言します。net10.0-windows を対象に -p:EnableWindowsTargeting=true -p:AnalysisMode=All でビルドし、AllowUnsafeBlocks とアセンブリ単位の DefaultDllImportSearchPaths を入れた状態で警告0件でした(これらがないときの SYSLIB1062・CA5392 の実際のメッセージは SetWindowText の記事に載せています)。
using System.Runtime.InteropServices;
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
const int VK_SHIFT = 0x10, VK_CAPITAL = 0x14;
short async = Native.GetAsyncKeyState(VK_SHIFT);
short queued = Native.GetKeyState(VK_CAPITAL);
Console.WriteLine($"Shift down now : {async < 0}"); // 最上位ビット = 符号ビット
Console.WriteLine($"Caps Lock on : {(queued & 1) != 0}"); // 最下位ビット = トグル
static partial class Native
{
[LibraryImport("user32.dll")]
internal static partial short GetAsyncKeyState(int vKey);
[LibraryImport("user32.dll")]
internal static partial short GetKeyState(int nVirtKey);
}
落とし穴が1つあります。戻り値の型を short ではなく int と誤って宣言しても、同じ設定で警告0件のままビルドが通りました。アナライザーは Windows のヘッダーとシグネチャを照合しないからです。ネイティブ関数が返すのは 16 ビットだけで、int で受けたときに上位ビットが符号拡張される保証はないため、< 0 の判定が意図どおりに動くとは限りません。戻り値はヘッダーどおり short にします。
まとめ
- 信頼できるのは最上位ビットだけ。
< 0か& 0x8000で確かめます。 GetAsyncKeyStateの最下位ビットは逆の意味で紹介されがちで、ドキュメントも頼らないよう求めています。GetKeyStateの最下位ビットは「以前押した」ではなくトグル状態です。- キーメッセージの中の修飾キーは
GetKeyState、メッセージと無関係な今の状態はGetAsyncKeyState。 - 入力処理はポーリングより
WM_KEYDOWN/WM_CHAR、デバイス単位が必要なら Raw Input。
ウィンドウのタイトルの読み書きは SetWindowText と GetWindowText、実行ファイルのフォルダーの取得は GetCurrentDirectory と GetModuleFileName の記事で扱っています。
よくある質問
GetAsyncKeyState の 0x8001 と 0x8000 は何が違いますか?
最上位ビット(0x8000)は呼び出した瞬間にキーが押されていること、最下位ビット(0x0001)は前回の GetAsyncKeyState 呼び出し以降にキーが押されたことを表します。2021年版の表はこの2つを逆に説明していました。ただし最下位ビットは他のプログラムの呼び出しに横取りされうるため Microsoft も頼らないよう求めており、& 0x8000(または < 0)だけを見るのが正解です。
GetKeyState と GetAsyncKeyState はどう使い分けますか?
WM_KEYDOWN などのキーメッセージを処理しながら Ctrl や Shift が押されていたかを知りたいなら GetKeyState です。そのメッセージが作られた時点の状態を返すからです。メッセージと無関係に今この瞬間の物理状態が必要なときだけ GetAsyncKeyState を使います。Caps Lock などのトグル状態は GetKeyState の最下位ビットでしか分かりません。
ループで GetAsyncKeyState を呼び続ける方式はなぜ勧められないのですか?
50ms ごとの確認では、その間に押して離した短い入力を取りこぼします。フォーカスに関係なくキーを読むので別のアプリで打ったキーにも反応し、何もしなくても起き続けます。ウィンドウのあるプログラムは WM_KEYDOWN を、ゲームなどデバイス単位の入力が必要なら Raw Input(WM_INPUT)を使います。