MapVirtualKeyExでShift文字(+など)を取得する方法:ToUnicodeExとGetKeyboardStateで仮想キーを正確に変換

Windowsで仮想キーコード(VK)から入力文字を一覧化しようとして、MapVirtualKeyEx(MAPVK_VK_TO_CHAR)で「=」は取れるのにShift時の「+」が取れない…という壁に当たることがあります。本記事では、OEMキーを含めて“修飾キー込み”で文字を取得する正しい方法を、実用コードと注意点つきで解説します。

目次

MapVirtualKeyExでShift文字が取れない理由

結論から言うと、MapVirtualKeyEx(MAPVK_VK_TO_CHAR)だけで「Shift押下時の文字」を取り出すのは難しいです。MapVirtualKeyExは「仮想キーを別の値へ変換する」ためのAPIですが、MAPVK_VK_TO_CHARは用途がかなり限定的で、Shift/Ctrl/Altなどの修飾キー状態を“入力として与える”仕組みがありません。

たとえば、US配列を前提にした代表例として、仮想キー 0xBB(VK_OEM_PLUS) を考えます。

  • Shiftなし:=
  • Shiftあり:+

この「同じVKなのに修飾キーで別の文字になる」という現象が、まさにMapVirtualKeyEx単体では扱いづらいポイントです。

項目MapVirtualKeyEx(MAPVK_VK_TO_CHAR)ToUnicodeEx
変換の目的VK→(基本的に“非シフト”の)文字コードVK+スキャンコード+キー状態→実際に入力されるUnicode
Shift/Ctrl/Altの指定不可(関数引数として与えられない)可能(256要素のキー状態配列を渡す)
キーボードレイアウト依存依存するが、表現力は限定的強く依存(レイアウトに沿ってより“現実の入力”へ近い)
デッドキー対応一部情報は取れるが扱いづらい対応(戻り値が負数=デッドキー)

つまり、「Shiftを押した状態の+が欲しい」「Ctrlの影響を除外したい」「CapsLockのせいで結果が変わるのを避けたい」といった実務的な要件が出た時点で、MapVirtualKeyEx(MAPVK_VK_TO_CHAR)だけでは限界が来ます。

正攻法はToUnicodeEx:修飾キー込みで“入力される文字”を得る

Windowsで「このVKを押したら(Shiftあり/なしで)どんな文字が入るか」を知りたいなら、基本はToUnicodeExです。ToUnicodeExは、次の3点セットで変換します。

  • 仮想キーコード(VK)
  • スキャンコード(物理キー位置に近い情報)
  • キー状態配列(256個のON/OFF):Shift/Ctrl/Alt/CapsLockなど

このキー状態配列を「Shiftが押されている状態」に書き換えて渡せば、Shift時の文字も取得できます。さらに、CtrlやCapsLockを意図的に無効化して、結果を安定させることも可能です。

全体の手順(Shiftあり/なしを切り替えて取得する)

  1. GetKeyboardLayoutで対象レイアウト(HKL)を取得
  2. GetKeyboardStateで現在のキー状態(256バイト)を取得
  3. 必要に応じて配列を編集(例:Shiftを押下状態、Ctrlを解除、CapsLockを無効化)
  4. MapVirtualKeyEx(MAPVK_VK_TO_VSC)でスキャンコードを取得
  5. ToUnicodeExでUnicode文字列を得る
API役割ここでの使い方
GetKeyboardLayoutキーボードレイアウト(HKL)を取得日本語/英語などのレイアウト差を反映する
GetKeyboardState256個のキー状態を取得Shift/Ctrl/Alt/CapsLock等を配列で操作する
MapVirtualKeyExVK→スキャンコード等へ変換ToUnicodeExに渡すスキャンコードを作る
ToUnicodeExVK+スキャンコード+状態→UnicodeShiftあり/なしを含め、最終的な文字を得る

ToUnicodeExの戻り値を正しく解釈する

ToUnicodeExは「文字が返ってくるかどうか」だけでなく、デッドキー(アクセント記号など)のような特殊ケースも返します。戻り値の扱いを間違えると、列挙時に結果が不安定になりがちなので、まずは意味を押さえておきましょう。

戻り値意味実務上の扱い
> 0返された文字数(通常1、場合により2)sbの先頭からその文字数分を採用
0その状態では文字にならない「文字なし」として扱う(空文字など)
< 0デッドキー(次の入力と合成されるキー)sb(0)のデッドキー文字を必要なら返す/列挙時は後述の“クリア”が重要

ここで重要なのは、戻り値が負数でもsbに文字が入る点です。「0以外ならsbから返す」という方針にすると、デッドキーも一覧化できます。

実装例(VB.NET):Shiftあり/なしを切り替えて文字を取得

以下は、仮想キーから「未シフト文字」「Shift文字」を取得するための、実務向けのVB.NET例です。ポイントは次の通りです。

  • MapVirtualKeyExはスキャンコード取得(MAPVK_VK_TO_VSC)に使う
  • GetKeyboardStateで取った配列を編集してShift押下をシミュレート
  • CtrlやCapsLockの影響を消して結果を安定化
  • デッドキーも返す(戻り値が0以外ならsb先頭を採用)
Imports System.Runtime.InteropServices
Imports System.Text

Public Module VkToChar

    Private Const MAPVK_VK_TO_VSC As UInteger = 0UI

    Private Const VK_SHIFT As Integer = &H10
    Private Const VK_CONTROL As Integer = &H11
    Private Const VK_CAPITAL As Integer = &H14
    Private Const VK_SPACE As Integer = &H20

    <DllImport("user32.dll")>
    Private Function GetKeyboardState(ByVal lpKeyState As Byte()) As Boolean
    End Function

    <DllImport("user32.dll")>
    Private Function GetKeyboardLayout(ByVal idThread As UInteger) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode)>
    Private Function MapVirtualKeyEx(ByVal uCode As UInteger, ByVal uMapType As UInteger, ByVal dwhkl As IntPtr) As UInteger
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Unicode)>
    Private Function ToUnicodeEx(
        ByVal wVirtKey As UInteger,
        ByVal wScanCode As UInteger,
        ByVal lpKeyState As Byte(),
        ByVal pwszBuff As StringBuilder,
        ByVal cchBuff As Integer,
        ByVal wFlags As UInteger,
        ByVal dwhkl As IntPtr
    ) As Integer
    End Function

    ''' <summary>
    ''' 仮想キーから、指定した修飾状態(例:Shiftあり/なし)で入力される文字を取得する。
    ''' 文字にならない場合は空文字を返す。
    ''' </summary>
    Public Function GetCharFromVirtualKey(ByVal virtualKey As UInteger, ByVal shiftPressed As Boolean) As String
        Dim hkl As IntPtr = GetKeyboardLayout(0UI)

        ' 現在の状態を取得してベースにする(必要ならここを全ゼロから組み立ててもOK)
        Dim keyState(255) As Byte
        If Not GetKeyboardState(keyState) Then
            Return ""
        End If

        ' Ctrlが押されていると文字にならないキーがあるため、押下状態を落として安定化
        keyState(VK_CONTROL) = CByte(keyState(VK_CONTROL) And &H7F)

        ' CapsLockの影響で結果が変わるのを避けたい場合は無効化
        keyState(VK_CAPITAL) = 0

        ' Shift押下をシミュレート(ビット7が「押下中」)
        If shiftPressed Then
            keyState(VK_SHIFT) = CByte(keyState(VK_SHIFT) Or &H80)
        Else
            keyState(VK_SHIFT) = CByte(keyState(VK_SHIFT) And &H7F)
        End If

        ' スキャンコードを作る
        Dim scanCode As UInteger = MapVirtualKeyEx(virtualKey, MAPVK_VK_TO_VSC, hkl)

        Dim sb As New StringBuilder(8)
        Dim result As Integer = ToUnicodeEx(virtualKey, scanCode, keyState, sb, sb.Capacity, 0UI, hkl)

        If result = 0 Then
            Return ""
        End If

        ' デッドキー(result < 0)の場合もsbには文字が入るので返す
        Dim s As String = sb.ToString()

        ' 列挙用途では、デッドキー状態が次の変換に影響することがあるためクリアしておく
        If result < 0 Then
            ClearDeadKeyState(hkl)
        End If

        ' resultが2のこともあるので安全に切り出す
        Dim n As Integer = Math.Min(Math.Abs(result), s.Length)
        If n <= 0 Then
            Return ""
        End If

        Return s.Substring(0, n)
    End Function

    ''' <summary>
    ''' ToUnicodeExが保持するデッドキー状態を軽く“空打ち”してクリアする。
    ''' </summary>
    Private Sub ClearDeadKeyState(ByVal hkl As IntPtr)
        Dim emptyState(255) As Byte
        Dim scSpace As UInteger = MapVirtualKeyEx(CUInt(VK_SPACE), MAPVK_VK_TO_VSC, hkl)
        Dim sb As New StringBuilder(8)
        ToUnicodeEx(CUInt(VK_SPACE), scSpace, emptyState, sb, sb.Capacity, 0UI, hkl)
    End Sub

End Module

この関数を使えば、次のように「未シフト/シフト」の両方を列挙できます。

For vk As Integer = 0 To 255
    Dim unshifted = VkToChar.GetCharFromVirtualKey(CUInt(vk), False)
    Dim shifted = VkToChar.GetCharFromVirtualKey(CUInt(vk), True)

    If unshifted <> "" OrElse shifted <> "" Then
        Console.WriteLine($"VK: {vk:X2}  Unshifted: '{unshifted}'  Shifted: '{shifted}'")
    End If
Next

US配列のVK_OEM_PLUS(0xBB)なら、概ね次のようなイメージになります(実際の結果はキーボードレイアウトに依存します)。

VK: BB  Unshifted: '='  Shifted: '+'

実装例(C#):同じ考え方でP/Invoke

開発現場ではC#で扱うことも多いので、同様の要点だけ押さえた最小構成の例も載せておきます。考え方はVB.NET版と同じで、「キー状態配列を作ってToUnicodeExへ渡す」が核です。

using System;
using System.Runtime.InteropServices;
using System.Text;

public static class VkToChar
{
    const uint MAPVK_VK_TO_VSC = 0;

    const int VK_SHIFT = 0x10;
    const int VK_CONTROL = 0x11;
    const int VK_CAPITAL = 0x14;
    const int VK_SPACE = 0x20;

    [DllImport("user32.dll")]
    static extern bool GetKeyboardState(byte[] lpKeyState);

    [DllImport("user32.dll")]
    static extern IntPtr GetKeyboardLayout(uint idThread);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    static extern int ToUnicodeEx(
        uint wVirtKey, uint wScanCode, byte[] lpKeyState,
        StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);

    public static string GetChar(uint vk, bool shiftPressed)
    {
        IntPtr hkl = GetKeyboardLayout(0);

        var state = new byte[256];
        if (!GetKeyboardState(state)) return "";

        // 安定化(必要に応じて)
        state[VK_CONTROL] = (byte)(state[VK_CONTROL] & 0x7F);
        state[VK_CAPITAL] = 0;

        if (shiftPressed) state[VK_SHIFT] = (byte)(state[VK_SHIFT] | 0x80);
        else state[VK_SHIFT] = (byte)(state[VK_SHIFT] & 0x7F);

        uint sc = MapVirtualKeyEx(vk, MAPVK_VK_TO_VSC, hkl);

        var sb = new StringBuilder(8);
        int rc = ToUnicodeEx(vk, sc, state, sb, sb.Capacity, 0, hkl);

        if (rc == 0) return "";

        if (rc < 0) ClearDeadKey(hkl);

        string s = sb.ToString();
        int n = Math.Min(Math.Abs(rc), s.Length);
        return n > 0 ? s.Substring(0, n) : "";
    }

    static void ClearDeadKey(IntPtr hkl)
    {
        var empty = new byte[256];
        uint scSpace = MapVirtualKeyEx((uint)VK_SPACE, MAPVK_VK_TO_VSC, hkl);
        var sb = new StringBuilder(8);
        ToUnicodeEx((uint)VK_SPACE, scSpace, empty, sb, sb.Capacity, 0, hkl);
    }
}

CtrlやCapsLockを無効化する理由(結果がブレる典型例)

「Shiftあり/なし」を列挙したいだけなのに、結果が空になったり、想定外の文字になったりすることがあります。原因として多いのが、CtrlキーとCapsLockの影響です。

Ctrlが押されていると“文字にならない”扱いになることがある

ToUnicodeExは「そのキーをその状態で押したら何が入力されるか」を返します。Ctrlが押されている状態だと、文字入力ではなくショートカット扱いになるキーがあり、結果が0(文字なし)になりやすくなります。列挙の目的が「入力可能な文字を取りたい」なら、Ctrlの押下ビットを落としてから変換すると安定します。

CapsLockは英字やShiftの判定をややこしくする

CapsLockがONの状態でShiftをシミュレートすると、英字の大小や一部記号の扱いが直感とズレることがあります。列挙ツールとして「Shiftで何が出るか」を機械的に取りたいなら、CapsLockのトグル状態を無効化しておくのが無難です。

影響要因起きがちな症状対策
Ctrl本来文字があるキーでも空になるVK_CONTROLの押下ビットを落とす
CapsLockShiftをシミュレートしたのに期待通りにならないVK_CAPITALを0にして無効化
実際のキーボード状態ユーザーが他のキーを押していると結果が変わる全ゼロ配列から必要な状態だけ作る、または取得した配列を強制整形

キーボードレイアウト差に注意:VK_OEM系は特に結果が変わる

OEMキー(VK_OEM_PLUS / VK_OEM_1 / VK_OEM_2 など)は、レイアウトや国・地域で割り当てが大きく変わるため、サンプルで「0xBBは=と+」と書かれていても、あなたの環境で同じとは限りません。

この点でToUnicodeExは有利です。HKL(キーボードレイアウトハンドル)を渡すことで、そのレイアウトに沿った“実際に入力される文字”を得られます。

  • 複数言語を切り替えるアプリなら、HKLを固定せず「対象スレッドのHKL」を使う
  • ツールとして特定レイアウトを評価したいなら、対象HKLを明示して呼び出す

また、同じレイアウトでも「IMEがONかどうか」で最終入力が変わるケースがありますが、ToUnicodeEx自体は“物理キーボード→Unicode”の変換を返すため、IMEの変換結果(かな漢字変換後)までを直接返すわけではありません。キー一覧を作る用途では、多くの場合これで十分です。

デッドキー(死にキー)を列挙するなら“状態クリア”が必須

欧文配列などでアクセント付き文字を入力するために使うキー(例:`´` や `^` のようなキー)は、単体では確定文字にならず、次の入力と合成されることがあります。これがデッドキーで、ToUnicodeExは戻り値を負数にして知らせます。

ここでの落とし穴は、ToUnicodeExがスレッド内でデッドキー状態を保持する点です。列挙ループでデッドキーを踏んだ直後のキー変換結果が、合成された別文字になってしまうことがあります。

対策として、本記事のサンプルのように、戻り値が負数だった場合はVK_SPACEなどを“空打ち”して状態をクリアすると安定します。

やりたいことよくある失敗おすすめ対策
デッドキーも一覧化したい負数を「文字なし」として捨ててしまう0以外はsbの文字を採用する
列挙結果を安定させたいデッドキー後に次のキーが合成される負数のときに空打ちして状態クリア

「MapVirtualKeyExだけでできないの?」に対する現実的な答え

質問でよくあるのが、「仮想キーコード+Shift押下状態」を指定して、MapVirtualKeyExだけで文字を取得できないのか?というものです。実務的には次のように考えるのが安全です。

  • MapVirtualKeyEx(MAPVK_VK_TO_CHAR)は、キー配列(Shift/Ctrl/Alt)を入力として受け取れない
  • 同じVKでも、修飾キーやレイアウトで文字が変わるケースでは表現力不足になりやすい
  • “実際に入力される文字”を得たいなら、ToUnicodeExが本命

補足として、Windowsには「文字→VK+Shift状態」を逆引きするVkKeyScanExというAPIもあります。ただしこれは「この文字を入力するにはどのVKと修飾が要るか」を調べるAPIで、今回のように「VK+修飾→文字」を網羅的に得る用途とは方向が逆です。

実務で使うときのチェックリスト

最後に、列挙ツールやショートカット表示、キーボードカスタマイズUIなどに組み込む際に、押さえておくと事故が減るポイントをまとめます。

チェック項目理由具体策
レイアウト(HKL)を意識しているかOEMキーの割り当てが環境で変わるGetKeyboardLayoutで取得し、ToUnicodeEx/MapVirtualKeyExへ同じHKLを渡す
Shift/Ctrl/Altを配列で制御しているか修飾キー込みの文字を得るためGetKeyboardState→配列編集→ToUnicodeEx
Ctrl/CapsLockの影響を排除する設計か列挙結果がブレる原因になるCtrl押下ビットを落とす、CapsLockを無効化する
デッドキー後の状態をクリアしているか次の変換が合成されて崩れるToUnicodeExの戻り値が負数なら空打ちして状態クリア
戻り値が2文字になるケースを考慮しているか一部入力は複数文字を返す可能性がある戻り値の絶対値分だけsbから切り出す

まとめ:Shift文字を取りたいならToUnicodeExが最短ルート

MapVirtualKeyEx(MAPVK_VK_TO_CHAR)は「仮想キー→文字」を返してくれる便利なAPIですが、Shift/Ctrl/Altなどの修飾キー状態を細かく指定できません。そのため、VK_OEM_PLUS(0xBB)のように、同じ仮想キーで「=」と「+」が切り替わるケースを正確に拾うには不向きです。

一方、ToUnicodeExはキー状態配列を受け取れるため、Shiftを押した状態をシミュレートして変換できます。さらに、CtrlやCapsLockを無効化して安定した列挙を行い、デッドキーも適切に扱えるようになります。仮想キーから入力文字を“正しく”取りたいなら、MapVirtualKeyEx(スキャンコード取得)+ToUnicodeEx(最終変換)の組み合わせを基本形として覚えておくと、応用が効きます。

この記事を書いた人

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

コメント

コメントする

目次