5

In a MFC application within PreTranslateMessage(MSG *pMsg) inherited from a CView, I have this:

if (pMsg->message == WM_KEYDOWN) ...

The fields in a WM_KEYDOWN are documented here. The virtual key VK_ value is in pMsg->wParam and pMsg->lParam contains several field, of which bits 16-23 is the keyboard scan code.

So in my code I use:

const int virtualKey = pMsg->wParam;
const int hardwareScanCode = (pMsg->lParam >> 16) & 0x00ff; // bits 16-23

On my non-US keyboard for example, when I press the "#" character, I get the following:

virtualKey == 0xde --> VK_OEM_7 "Used for miscellaneous characters; it can vary by keyboard."
hardwareScanCode == 0x29 (41 decimal)

The character I'd like to "capture" or process differently is ASCII "#", 0x23 (35 decimal).

MY QUESTION

How do I translate the WM_KEYDOWN information to get something I can compare against, regardless of language or keyboard layout? I need to identify the # key whether the user has a standard US keyboard, or something different.

For example, I've been looking at the following functions such as:

MapVirtualKey(virtualkey, MAPVK_VSC_TO_VK);
// previous line is useless, the key VK_OEM_7 doesn't map to anything without the scan code

ToAscii(virtualKey, hardwareScanCode, nullptr, &word, 0);
// previous line returns zero, and zero is written to `word`

Edit:

Long story short: On a U.S. keyboard, SHIFT+3 = #, while on a French keyboard SHIFT+3 = /. So I don't want to look at individual keys, instead I want to know about the character.

When handling WM_KEYDOWN, how do I translate lParam and wParam (the "keys") to find out the character which the keyboard and Windows is about to generate?

IInspectable
  • 46,945
  • 8
  • 85
  • 181
Stéphane
  • 19,459
  • 24
  • 95
  • 136
  • This is confusing because the question conflates _keys_ with _characters_, so I don't really understand which you're after. – Adrian McCarthy Jun 20 '17 at 20:03
  • I see why you are confused. On a French-Canadian keyboard, there is a key specifically for the '#' character. Just like there is a key for the 'A' character, etc. So the key and the character are the same thing. I'd forgotten that on a U.S. keyboard that is not the case. – Stéphane Jun 20 '17 at 21:07
  • 1
    If you want to know the character, you need to watch the WM_CHAR (or related) messages. But I don't think those go through pre-translate. Rather, they are generated by TranslateMessage. – Adrian McCarthy Jun 20 '17 at 22:09
  • I need to catch it in pre-translate so I can consume certain keyboard events and prevent them from reaching the underlying window. – Stéphane Jun 20 '17 at 22:11
  • 1
    If it's one particular window (or window class), you could subclass the window to intercept WM_CHAR and pass everything else on. – Adrian McCarthy Jun 20 '17 at 23:04

3 Answers3

7

I believe this is a better solution. This one was tested with both the standard U.S. keyboard layout and the Canadian-French keyboard layout.

const int wParam = pMsg->wParam;
const int lParam = pMsg->lParam;
const int keyboardScanCode = (lParam >> 16) & 0x00ff;
const int virtualKey = wParam;

BYTE keyboardState[256];
GetKeyboardState(keyboardState);

WORD ascii = 0;
const int len = ToAscii(virtualKey, keyboardScanCode, keyboardState, &ascii, 0);
if (len == 1 && ascii == '#')
{
    // ...etc...
}

Even though the help page seems to hint that keyboardState is optional for the call to ToAscii(), I found that it was required with the character I was trying to detect.

Stéphane
  • 19,459
  • 24
  • 95
  • 136
  • That actually looks like a sane solution. I never had a need to call `ToAscii`, but this appears to be a perfect use case. I can't think of a case, where this test would fail and produce a false positive or false negative. – IInspectable Jun 22 '17 at 09:17
  • For detecting `'#'`, this is sufficient. If you wanted to detect an arbitrary character, it lacks generality for some corner cases. You could use ToUnicode, but even that one carries the caveat: "The parameters supplied to the ToUnicode function might not be sufficient to translate the virtual-key code...." – Adrian McCarthy Jun 22 '17 at 17:14
0

Found the magic API call that gets me what I need: GetKeyNameText()

if (pMsg->message == WM_KEYDOWN)
{
    char buffer[20];
    const int len = GetKeyNameTextA(pMsg->lParam, buffer, sizeof(buffer));
    if (len == 1 && buffer[0] == '#')
    {
        // ...etc...
    }
}

Nope, that code only works on keyboard layouts that have an explicit '#' key. Doesn't work on layouts like the standard U.S. layout where '#' is a combination of other keys like SHIFT + 3.

Stéphane
  • 19,459
  • 24
  • 95
  • 136
  • "I need to identify the # key whether the user has a standard US keyboard, or something different." But a U.S. keyboard doesn't have a `#` key. You can type a `#` _character_ by the combination of Shift and the `3` key, but this code won't tell you that. – Adrian McCarthy Jun 20 '17 at 20:10
  • @AdrianMcCarthy That's worrisome. I don't have a U.S. keyboard layout, just a French-Canadian one where "#" by itself is a key. I'm going to have to get a "standard" U.S. keyboard so I can find a solution that works regardless of the keyboard type. – Stéphane Jun 20 '17 at 20:59
  • 1
    You don't have to get a physical keyboard to test this. Simply install a US English keyboard *layout*. – IInspectable Jun 20 '17 at 21:17
0

I'm not an MFC expert, but here's roughly what I believe its message loop looks like:

while (::GetMessage(&msg, NULL, 0, 0) > 0) {
    if (!app->PreTranslateMessage(&msg)) {    // the hook you want to use
        TranslateMessage(&msg);  // where WM_CHAR messages are generated
        DispatchMessage(&msg);  // where the original message is dispatched
    }
}

Suppose a U.S. user (for whom 3 and # are on the same key) presses that key.

The PreTranslateMessage hook will see the WM_KEYDOWN message.

If it allows the message to pass through, then TranslateMessage will generate a WM_CHAR message (or something from that family of messages) and dispatch it directly. PreTranslateMessage will never see the WM_CHAR.

Whether that WM_CHAR is a '3' or a '#' depends on the keyboard state, specifically whether a Shift key is currently pressed. But the WM_KEYDOWN message doesn't contain all the keyboard state. TranslateMessage keeps track of the state by taking notes on the keyboard messages that pass through it, so it knows whether the Shift (or Ctrl or Alt) is already down.

Then DispatchMessage will dispatch the original WM_KEYDOWN message.

If you want to catch only the '#' and not the '3's, then you have two options:

  1. Make your PreTranslateMessage hook keep track of all the keyboard state (like TranslateMessage would normally do). It would have to watch for all of the keyboard messages to track the keyboard state and use that in conjunction with the keyboard layout to figure whether the current message would normally generate a '#'. You'd then have to manually dispatch the WM_KEYDOWN message and return TRUE (so that the normal translate/dispatch doesn't happen). You'd also have to be careful to also filter the corresponding WM_KEYUP messages so that you don't confuse TranslateMessage's internal state. That's a lot of work and lots to test.

  2. Find a place to intercept the WM_CHAR messages that TranslateMessage generates.

For that second option, you could subclass the destination window, have it intercept WM_CHAR messages when the character is '#' and pass everything else through. That seems a lot simpler and well targeted.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
  • 1
    The system tracks keyboard state for you already. If you need to know the (synchronous) keystate, call the [GetKeyState](https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301.aspx) API. There's no need to further complicate things by tracking keyboard state yourself. – IInspectable Jun 21 '17 at 07:47
  • I'd still argue that option 2 is less work and far easier to test. – Adrian McCarthy Jun 21 '17 at 16:13
  • I didn't comment on the relative difficulty of the approaches. – IInspectable Jun 21 '17 at 16:17
  • I was verbose on option 1 to emphasize how much work is involved and how--even if the OS provides some APIs to help--this effectively involves duplicating some work that the OS already does. I just want to make sure that people don't read about GetKeyState and assume that it simplifies option 1 to the point that it becomes a reasonable alternative to option 2. – Adrian McCarthy Jun 21 '17 at 16:28
  • There is no reason to make option 1 sound more complicated than it is. You can still make a point, that it *is* complex, and very hard to get right. Without misconstruing the work involved. If you explained the difficulty in finding out, which key press(es) (and combinations) produce a given character, using the current keyboard layout, you'd be making a stronger point, instead of suggesting that it were required to monitor state the system already monitors for you. – IInspectable Jun 21 '17 at 16:35
  • 1
    Why not use ToAscii() to obtain the character as I wrote in my solution yesterday and avoid both option 1 and 2? – Stéphane Jun 21 '17 at 23:14