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あり/なしを切り替えて取得する)
- GetKeyboardLayoutで対象レイアウト(HKL)を取得
- GetKeyboardStateで現在のキー状態(256バイト)を取得
- 必要に応じて配列を編集(例:Shiftを押下状態、Ctrlを解除、CapsLockを無効化)
- MapVirtualKeyEx(MAPVK_VK_TO_VSC)でスキャンコードを取得
- ToUnicodeExでUnicode文字列を得る
| API | 役割 | ここでの使い方 |
|---|---|---|
| GetKeyboardLayout | キーボードレイアウト(HKL)を取得 | 日本語/英語などのレイアウト差を反映する |
| GetKeyboardState | 256個のキー状態を取得 | Shift/Ctrl/Alt/CapsLock等を配列で操作する |
| MapVirtualKeyEx | VK→スキャンコード等へ変換 | ToUnicodeExに渡すスキャンコードを作る |
| ToUnicodeEx | VK+スキャンコード+状態→Unicode | Shiftあり/なしを含め、最終的な文字を得る |
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の押下ビットを落とす |
| CapsLock | Shiftをシミュレートしたのに期待通りにならない | 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(最終変換)の組み合わせを基本形として覚えておくと、応用が効きます。

コメント