MapVirtualKey has a known broken behaviour.
The docs are lying you about MAPVK_VK_TO_CHAR or 2 mode.
According to experiments and leaked Windows XP source code (in \windows\core\ntuser\kernel\xlate.c file) it contains different behaviour for 'A'..'Z' VKs (those VKs are specifically not defined in Win32 API WinUser.h header and are equivalent to 'A'..'Z' ASCII chars):
case 2:
/*
* Bogus Win3.1 functionality: despite SDK documenation, return uppercase for
* VK_A through VK_Z
*/
if ((wCode >= (WORD)'A') && (wCode <= (WORD)'Z')) {
return wCode;
}
Not sure why MS decided to pull this bug from Win 3.1 but current situation on my Windows 10 is like this.
Also some keyboard layouts can emit multiple WCHAR characters on a single key press (UTF-16 surrogate pairs or ligatures that can contain multiple Unicode code points). MapVirtualKey with MAPVK_VK_TO_CHAR is failing to return proper values for these keys too - it will return U+F002 code point in this case.
As a workaround I can recommend you to use ToUnicode[Ex] API that can do this mapping for you:
// Returns UTF-8 string
std::string GetStrFromKeyPress(uint16_t scanCode, bool isShift)
{
static BYTE keyboardState[256];
memset(keyboardState, 0, 256);
if (isShift)
{
keyboardState[VK_SHIFT] |= 0x80;
}
wchar_t chars[5] = { 0 };
const UINT vkCode = ::MapVirtualKeyW(scanCode, MAPVK_VSC_TO_VK_EX);
// This call can produce multiple UTF-16 code points
// in case of ligatures or non-BMP Unicode chars that have hi and low surrogate
// See examples: https://kbdlayout.info/features/ligatures
int code = ::ToUnicode(vkCode, scanCode, keyboardState, chars, 4, 0);
if (code < 0)
{
// Dead key
if (chars[0] == 0 || std::iswcntrl(chars[0])) {
return {};
}
code = -code;
}
// Clear keyboard state
{
memset(keyboardState, 0, 256);
const UINT clearVkCode = VK_DECIMAL;
const UINT clearScanCode = ::MapVirtualKeyW(clearVkCode, MAPVK_VK_TO_VSC);
wchar_t tmpChars[5] = { 0 };
do {} while (::ToUnicode(clearVkCode, clearScanCode, keyboardState, tmpChars, 4, 0) < 0);
}
// Do not return control characters
if (code <= 0 || (code == 1 && std::iswcntrl(chars[0]))) {
return {};
}
return utf8::narrow(chars, code);
}
Or even better: if you have Win32 message loop - just use TranslateMessage() (that calls ToUnicode() under the hood) and then process WM_CHAR message.
PS: Same applies to GetKeyNameText API since it calls the MapVirtualKey(vk, MAPVK_VK_TO_CHAR) under the hood for keys that do not have explicit name set in keyboard layout dll (usually only non-characters do have names).