C/C++(Windows)で矢印キー入力を検知して画像を動かす方法【Win32 APIとWindows Forms解説】

Windows の C/C++ で「矢印キーでキャラクター(画像)を動かす」のは、ゲームやツールでよくあるテーマですが、Win32 API と .NET(Windows Forms)で考え方がまったく違うため、最初に混乱しがちです。本記事では、Win32 ネイティブを中心に、矢印キー押下を検知して画面上の画像(矩形)を動かす方法を、実行可能なコード付きで丁寧に解説します。

目次

C/C++(Windows)で矢印キー入力を検知する全体像

まずは「どの種類のアプリで何をやるのか」を整理しておきます。

観点Win32 ネイティブ C/C++Windows Forms(C++/CLI)
プロジェクト種別Win32 プロジェクト / Windows デスクトップアプリ.NET Framework / .NET の Windows フォームアプリ
キー入力の基本ウィンドウプロシージャで WM_KEYDOWN / WM_KEYUP を処理フォームやコントロールの KeyDown イベント、ProcessCmdKey
矢印キーのコードVK_UP, VK_DOWN, VK_LEFT, VK_RIGHTKeys::Up, Keys::Down など
画像を動かす流れ座標変数を更新 → 再描画(InvalidateRect → WM_PAINT)同じく座標更新 → Invalidate / OnPaint で描画

この記事でメインに説明するのは「Win32 ネイティブ C/C++」です。最後に Windows Forms(C++/CLI)で IsInputKey を見かけた人向けの補足も行います。

Win32 API で矢印キーを受け取る基本

メッセージループとウィンドウプロシージャ(WndProc)の役割

Win32 アプリでは、キー入力は「メッセージ(WM_KEYDOWN など)」としてウィンドウに届きます。メイン関数(WinMain)にはメッセージループがあり、ここからウィンドウプロシージャ(WndProc)にメッセージが渡されます。

最小限の骨組みは次のようになります。


#include <windows.h>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
    // ウィンドウクラス登録
    WNDCLASS wc = {};
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = TEXT("ArrowKeySampleClass");
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

    if (!RegisterClass(&wc)) {
        return 0;
    }

    // ウィンドウ作成
    HWND hWnd = CreateWindow(
        wc.lpszClassName,
        TEXT("C/C++ Arrow Key Sample"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        640, 480,
        NULL, NULL, hInstance, NULL);

    if (!hWnd) {
        return 0;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // メッセージループ
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);   // キー入力などを変換
        DispatchMessage(&msg);    // WndProc へ送る
    }
    return (int)msg.wParam;
}

実際に矢印キーを処理するのは WndProc の中です。

WM_KEYDOWN / WM_KEYUP と仮想キーコード(VK_)

キーボードの押下は主に次のメッセージで届きます。

  • WM_KEYDOWN:キーが押された
  • WM_KEYUP:キーが離された

どのキーかは wParam に入っている「仮想キーコード(Virtual-Key Code)」で判別できます。

キー定数名16進コード
上矢印VK_UP0x26
下矢印VK_DOWN0x28
左矢印VK_LEFT0x25
右矢印VK_RIGHT0x27

これらの定数は windows.h をインクルードすると winuser.h 経由で自動的に使えるようになります。

矢印キーで矩形(画像)を動かす最小サンプル

ここでは「画像の代わりに矩形」を描画し、その位置を矢印キーで動かす最低限のサンプルを示します。


#include <windows.h>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
    WNDCLASS wc = {};
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = TEXT("ArrowKeySampleClass");
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

    if (!RegisterClass(&wc)) return 0;

    HWND hWnd = CreateWindow(
        wc.lpszClassName,
        TEXT("C/C++ Arrow Key Sample"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        640, 480,
        NULL, NULL, hInstance, NULL);

    if (!hWnd) return 0;

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static int x = 100;
    static int y = 100;
    const int step = 5;   // 移動量(ピクセル)

    switch (msg) {
    case WM_KEYDOWN:
        switch (wParam) {
        case VK_UP:    y -= step; break;
        case VK_DOWN:  y += step; break;
        case VK_LEFT:  x -= step; break;
        case VK_RIGHT: x += step; break;
        }
        InvalidateRect(hWnd, NULL, TRUE);  // 再描画要求
        return 0;

    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);

        // ここでは簡易的に矩形を描画
        RECT rect = { x, y, x + 50, y + 50 };
        HBRUSH hBrush = (HBRUSH)GetStockObject(GRAY_BRUSH);
        FillRect(hdc, &rect, hBrush);

        EndPaint(hWnd, &ps);
        return 0;
    }

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hWnd, msg, wParam, lParam);
}

上記のサンプルでは、矢印キーが押されるたびに x / y を変更し、InvalidateRect で再描画を促しています。実際の画像(ビットマップや PNG)を描画したい場合は、WM_PAINT 内で BitBlt や GDI+ の描画処理に置き換えれば構造は同じです。

画像を動かす設計を一段階進める

上のサンプルは「キーが押された瞬間だけ動く」単発タイプでした。ゲームっぽく「押している間ずっと移動する」「斜め移動する」といったことをしたい場合は、少し設計を拡張します。

座標と速度をまとめた構造体を定義する


struct Player
{
    int x;
    int y;
    int vx;
    int vy;
};

static Player g_player = { 100, 100, 0, 0 };

キー入力で vx / vy(速度)を変更し、別途「更新処理」で実座標に加算する、という形にすると拡張しやすくなります。

タイマーやゲームループで位置を更新する

ウィンドウメッセージだけでなく、「一定時間ごとに更新する」仕組みも加えます。

簡単な方法は SetTimer を使うことです(本格的なゲームでは高精度タイマーや独自ループを使うことが多いですが、ここでは説明をシンプルにします)。


#define IDT_GAMELOOP 1

// WinMain 内でウィンドウ作成後にタイマー開始
SetTimer(hWnd, IDT_GAMELOOP, 16, NULL);  // 約60FPS程度

// WndProc 内
case WM_TIMER:
    if (wParam == IDT_GAMELOOP) {
        g_player.x += g_player.vx;
        g_player.y += g_player.vy;
        InvalidateRect(hWnd, NULL, TRUE);
    }
    return 0;

矢印キーの押し / 離しで速度を変えるようにすると、キーを押している間だけ動く挙動になります。


case WM_KEYDOWN:
    switch (wParam) {
    case VK_UP:    g_player.vy = -4; break;
    case VK_DOWN:  g_player.vy =  4; break;
    case VK_LEFT:  g_player.vx = -4; break;
    case VK_RIGHT: g_player.vx =  4; break;
    }
    return 0;

case WM_KEYUP:
    switch (wParam) {
    case VK_UP:
    case VK_DOWN:
        g_player.vy = 0;
        break;
    case VK_LEFT:
    case VK_RIGHT:
        g_player.vx = 0;
        break;
    }
    return 0;

これで「押しっぱなしで連続移動」「矢印キーを離せば停止」という自然な動きになります。

画面外に出ないように制限する(境界チェック)

プレイヤーの座標が画面外に飛び出すと見た目がおかしくなります。WM_TIMER で位置を更新したあとに、ウィンドウサイズと画像サイズを使って座標をクリップします。


void ClampPlayer(HWND hWnd)
{
    RECT rc;
    GetClientRect(hWnd, &rc);

    const int width  = 50;  // プレイヤー画像の幅
    const int height = 50;  // プレイヤー画像の高さ

    if (g_player.x < 0) g_player.x = 0;
    if (g_player.y < 0) g_player.y = 0;

    if (g_player.x + width  > rc.right)
        g_player.x = rc.right - width;

    if (g_player.y + height > rc.bottom)
        g_player.y = rc.bottom - height;
}

// WM_TIMER 内で位置更新後に呼ぶ
case WM_TIMER:
    if (wParam == IDT_GAMELOOP) {
        g_player.x += g_player.vx;
        g_player.y += g_player.vy;
        ClampPlayer(hWnd);
        InvalidateRect(hWnd, NULL, TRUE);
    }
    return 0;

キーリピートと WM_KEYDOWN の lParam を理解する

Windows では、キーを押しっぱなしにしていると一定間隔で WM_KEYDOWN が連続で飛んできます。これを「キーリピート」と呼びます。

単純に「キーが押された瞬間だけ何かしたい」場合は、WM_KEYDOWN の lParam を使って「初回かどうか」を判定できます。

ビット位置意味
ビット 0〜15リピート回数
ビット 16〜23スキャンコード
ビット 24拡張キーかどうか
ビット 30前回状態(0: 直前は押されていない / 1: 既に押されていた)
ビット 31現在の状態(0: 押された / 1: 離された)

新規押下だけを検出したい場合の典型的な書き方は次のようになります。


case WM_KEYDOWN:
{
    BOOL isFirst = ((lParam & 0x40000000) == 0); // ビット30が0なら初回
    if (!isFirst) {
        // リピート分は無視したい場合は return
        return 0;
    }

    switch (wParam) {
    case VK_UP:
        // 「押された瞬間だけ」処理したいコード
        break;
    }
    return 0;
}

ゲームや連続移動ではリピートを気にせず WM_KEYDOWN の度に速度を設定し、WM_KEYUP で止める設計のほうがシンプルです。一方、メニュー選択やショートカットキーなど「1回だけ実行したい」場面では、上記のように初回判定を入れると意図どおりの動作になります。

複数キー同時押し(斜め移動など)を実現する方法

上下左右を同時に押して斜め移動したい場合、WM_KEYDOWN/WM_KEYUP だけでは「どのキーが押しっぱなしなのか」を自前で管理する必要があります。

押下状態フラグを自前で管理する方法


static bool g_pressedUp    = false;
static bool g_pressedDown  = false;
static bool g_pressedLeft  = false;
static bool g_pressedRight = false;

case WM_KEYDOWN:
    switch (wParam) {
    case VK_UP:    g_pressedUp    = true; break;
    case VK_DOWN:  g_pressedDown  = true; break;
    case VK_LEFT:  g_pressedLeft  = true; break;
    case VK_RIGHT: g_pressedRight = true; break;
    }
    return 0;

case WM_KEYUP:
    switch (wParam) {
    case VK_UP:    g_pressedUp    = false; break;
    case VK_DOWN:  g_pressedDown  = false; break;
    case VK_LEFT:  g_pressedLeft  = false; break;
    case VK_RIGHT: g_pressedRight = false; break;
    }
    return 0;

そして、位置更新時にこれらのフラグから速度を決めます。


void UpdateVelocity()
{
    g_player.vx = 0;
    g_player.vy = 0;

    const int speed = 4;

    if (g_pressedUp)    g_player.vy -= speed;
    if (g_pressedDown)  g_player.vy += speed;
    if (g_pressedLeft)  g_player.vx -= speed;
    if (g_pressedRight) g_player.vx += speed;
}

// WM_TIMER などで毎フレーム呼ぶ
case WM_TIMER:
    if (wParam == IDT_GAMELOOP) {
        UpdateVelocity();
        g_player.x += g_player.vx;
        g_player.y += g_player.vy;
        ClampPlayer(hWnd);
        InvalidateRect(hWnd, NULL, TRUE);
    }
    return 0;

このようにすれば、上下左右を同時に押すことで自然に斜め移動ができるようになります。

GetAsyncKeyState を使ったポーリング方式

別の方法として、毎フレーム GetAsyncKeyState でキー状態を問い合わせる方法もあります。


void UpdateVelocityWithAsyncKey()
{
    g_player.vx = 0;
    g_player.vy = 0;

    const int speed = 4;

    if (GetAsyncKeyState(VK_UP)    & 0x8000) g_player.vy -= speed;
    if (GetAsyncKeyState(VK_DOWN)  & 0x8000) g_player.vy += speed;
    if (GetAsyncKeyState(VK_LEFT)  & 0x8000) g_player.vx -= speed;
    if (GetAsyncKeyState(VK_RIGHT) & 0x8000) g_player.vx += speed;
}

ゲームループの中で毎フレームこの関数を呼び出せば、WM_KEYDOWN/UP のフラグ管理を省略できます。ただし、GetAsyncKeyState 乱用はコードがメッセージ駆動でなくなっていくので、用途に応じて使い分けるのがおすすめです。

フォーカスとキーボードメッセージの流れ

矢印キーが反応しないとき、よくある原因が「フォーカスが別のコントロールに行っている」ことです。

  • キーボードメッセージは基本的に「フォーカスを持っているウィンドウ/コントロール」に届く
  • テキストボックスなどの子ウィンドウがフォーカスを持っていると、メインウィンドウの WndProc には WM_KEYDOWN が来ない

ゲームやツールで常にキーボードをメインウィンドウで処理したい場合は、以下のような対策が考えられます。

  • フォーカスを奪うコントロールを置かない(フルスクリーンゲームなど)
  • メインウィンドウにフォーカスを戻す(クリック時に SetFocus(hWnd) を呼ぶなど)
  • サブクラス化やアクセラレータテーブルを使って共通処理にする

MFC を使っている場合は、CWinApp::PreTranslateMessage を通して共通でキーを拾う方法もありますが、素の Win32 API では上記の方針で十分なことが多いです。

コンソールアプリで矢印キーを扱う場合

ここまでの説明は「GUI アプリ(Win32 ウィンドウ)」向けでした。コンソールアプリ(黒い画面)で矢印キーを取りたい場合は、<conio.h> の _kbhit と _getch を使うのが簡単です。

矢印キーは「拡張キー」として扱われるため、2回読み出す必要があります。


#include &lt;conio.h&gt;
#include &lt;stdio.h&gt;

int main()
{
    printf("矢印キーで操作、ESCで終了\n");

    bool running = true;
    while (running) {
        if (_kbhit()) {
            int ch = _getch();
            if (ch == 0 || ch == 0xE0) {
                // 拡張コード
                int ch2 = _getch();
                switch (ch2) {
                case 72:  // 上矢印
                    printf("UP\n");
                    break;
                case 80:  // 下矢印
                    printf("DOWN\n");
                    break;
                case 75:  // 左矢印
                    printf("LEFT\n");
                    break;
                case 77:  // 右矢印
                    printf("RIGHT\n");
                    break;
                }
            } else {
                if (ch == 27) { // ESC
                    running = false;
                }
            }
        }
    }
    return 0;
}

コンソールで簡易的なゲームを作るときにはこのパターンを使うことが多いです。ただし、Windows の標準コンソールは描画が遅めなので、本格的なアクションゲームを作るには GUI アプリか別のライブラリ(SDL, DirectX など)を検討したほうがよいでしょう。

Windows Forms(C++/CLI)で IsInputKey を見かけた場合

質問文に出てきた IsInputKey は、実は「Win32 ネイティブ」ではなく「Windows Forms(.NET)」の世界のメソッドです。C++/CLI で Windows フォームアプリを作っている場合に登場します。

同じ「C++」という名前でも、Win32 ネイティブと C++/CLI は土台がまったく違うので注意が必要です。

フォーム全体で矢印キーを処理する(KeyPreview + KeyDown)

最も簡単なやり方は、フォームの KeyPreview プロパティを true にし、フォームの KeyDown イベントで Keys::Up などを判定する方法です。


public ref class MainForm : public System::Windows::Forms::Form
{
public:
    MainForm()
    {
        InitializeComponent();
        this-&gt;KeyPreview = true; // フォームが先にキーを受け取る
        this-&gt;KeyDown += gcnew KeyEventHandler(this, &amp;MainForm::OnKeyDown);
    }

private:
    void OnKeyDown(System::Object^ sender, System::Windows::Forms::KeyEventArgs^ e)
    {
        if (e-&gt;KeyCode == Keys::Up) {
            // 上矢印キーの処理(画像の Y 座標を減らすなど)
        } else if (e-&gt;KeyCode == Keys::Down) {
            // 下矢印キー
        }
    }
};

KeyPreview = true を設定しないと、フォーカスを持つコントロールが矢印キーを消費してしまい、フォーム側で受け取れないことがあります。

ProcessCmdKey をオーバーライドしてショートカット風に処理する

フォーム全体のショートカットとして矢印キーを処理したいなら、ProcessCmdKey をオーバーライドする方法もよく使われます。


protected:
    virtual bool ProcessCmdKey(Message% msg, Keys keyData) override
    {
        switch (keyData) {
        case Keys::Up:
            // 上矢印キー
            return true; // 処理済み

        case Keys::Down:
            // 下矢印キー
            return true;
        }
        return __super::ProcessCmdKey(msg, keyData);
    }

ここで true を返すと「フォーム側で処理済み」となり、他のコントロールには届きません。

IsInputKey と PreviewKeyDown の使いどころ

一部のコントロール(ボタンやチェックボックスなど)は、矢印キーを「フォーカス移動のためのナビゲーションキー」とみなします。この場合、矢印キーは通常のキー入力としては扱われません。

そのようなときに登場するのが IsInputKey と PreviewKeyDown です。

  • IsInputKey:カスタムコントロールでオーバーライドして「このキーは入力キーとして扱ってほしい」と宣言する
  • PreviewKeyDown:矢印キーなども先に通知してもらい、必要なら「入力キー扱い」に変更する

カスタムコントロールの場合の IsInputKey の例:


public ref class MyControl : public System::Windows::Forms::Control
{
protected:
    virtual bool IsInputKey(Keys keyData) override
    {
        if (keyData == Keys::Up || keyData == Keys::Down ||
            keyData == Keys::Left || keyData == Keys::Right) {
            return true; // ナビゲーションではなく「入力」として扱う
        }
        return __super::IsInputKey(keyData);
    }
};

ただし、単に「矢印キーで画像を動かしたい」だけなら、通常は KeyPreview + KeyDown か ProcessCmdKey で十分なことがほとんどです。IsInputKey は「コントロール単体で矢印キーを奪いたい」ような、少し高度な場面で必要になることが多いです。

グローバルに矢印キーを取得したい場合の注意点

アプリがアクティブでないときも矢印キーを取得したい(例:常駐ツールでゲームの入力を監視したい)という需要もあります。この場合は、次のような方法が検討されます。

  • 低レベルキーボードフック(WH_KEYBOARD_LL)
  • Raw Input(RegisterRawInputDevices)

ただし、どちらも「システム全体の入力」に介入する強力な仕組みです。実装を誤ると他アプリの操作に悪影響を与えかねないため、以下に注意してください。

  • 本当にグローバルフックが必要かを再検討する(通常のゲームやツールなら、アクティブ時だけ取得できれば十分なことが多い)
  • 処理はできるだけ軽くし、時間のかかる処理は別スレッドに回す
  • ウイルス対策ソフトや OS の保護機能に引っかかる可能性があることを理解する

「自分のアプリ内の画像を動かしたいだけ」であれば、まずはグローバルフックを使わない設計をおすすめします。

ありがちなハマりポイントとチェックリスト

矢印キーで画像を動かすだけのことでも、実際にやってみると意外なところでハマりがちです。よくあるパターンをチェックリスト形式でまとめます。

症状原因候補確認ポイント
矢印キーがまったく反応しないコンソールアプリで Win32 メッセージを見ている / フォーカスが別コントロールプロジェクト種別は「Windows アプリ」か?
クリックしてメインウィンドウにフォーカスがあるか?
矢印キーで動くが、たまに動かない方向がある速度の設定/解除ロジックに漏れがあるWM_KEYDOWN / WM_KEYUP の両方に全ての方向の処理が書かれているか?
押した瞬間だけ動き、押しっぱなしで連続しないタイマーやゲームループでの更新がないSetTimer や自前ループで「毎フレーム座標更新」をしているか?
矢印キーを押すとフォーカスが移動してしまうWindows Forms のナビゲーションキー扱いKeyPreview を true にしているか?
必要なら IsInputKey をオーバーライドしているか?

まとめ:C/C++(Windows)で矢印キー入力と画像移動を実装するポイント

最後に、本記事で解説した内容を要点だけ整理します。

  • ネイティブ C/C++(Win32)の場合
    • WndProc で WM_KEYDOWN / WM_KEYUP を受け取る
    • wParam の仮想キーコード VK_UP / VK_DOWN / VK_LEFT / VK_RIGHT で判定する
    • 座標変数を更新し、InvalidateRect で再描画をトリガーする
    • 連続移動や斜め移動は「押下状態フラグ」や GetAsyncKeyState で実現する
    • 画面外に出ないようにクライアント領域サイズを使って境界チェックする
  • キーリピート制御
    • lParam のビット30(前回状態)を見れば「押された瞬間だけ」を判定できる
    • ゲームのように押しっぱなしで移動したい場合は、リピートを気にせず速度更新+タイマーで位置更新するほうがシンプル
  • コンソールアプリの場合
    • _kbhit() と _getch() を使い、矢印キーは 2回読み出す(拡張コード)
  • Windows Forms(C++/CLI)の場合
    • IsInputKey は .NET コントロールの話であり、ネイティブ Win32 では使わない
    • まずは KeyPreview = true + Form::KeyDown または ProcessCmdKey で矢印キーを処理する
    • どうしてもコントロール単体で矢印キーを入力キーにしたい場合に IsInputKey をオーバーライドする

「矢印キーで画像を動かす」という一見シンプルな要件も、Win32 と .NET の違いやメッセージ駆動の仕組みを理解して実装すると、応用範囲が一気に広がります。まずはこの記事の最小サンプルをそのままコピーして動かし、そこから画像読み込みやアニメーションなどを少しずつ追加していくと、C/C++ での Windows プログラミングの感覚がつかみやすくなるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次