If you read the MSDN documentation of WM_KEYDOWN and WM_KEYUP you can see that those message require us to interpret lParam as a bitfield:

lParam
Specifies the repeat count, scan code, extended-key flag, context code, previous key-state flag, and transition-state flag, as shown in the following table.

0-15
Specifies the repeat count for the current message. The value is the number of times the keystroke is autorepeated as a result of the user holding down the key. The repeat count is always one for a WM_KEYUP message.
16-23
Specifies the scan code. The value depends on the OEM.
24
Specifies whether the key is an extended key, such as the right-hand ALT and CTRL keys that appear on an enhanced 101- or 102-key keyboard. The value is 1 if it is an extended key; otherwise, it is 0.
25-28
Reserved; do not use.
29
Specifies the context code. The value is always 0 for a WM_KEYUP message.
30
Specifies the previous key state. The value is always 1 for a WM_KEYUP message.
31
Specifies the transition state. The value is always 1 for a WM_KEYUP message.

I was looking for a convenient way to get and read the bits and this is what I made up:

Note that I use a set to emulate a bitfield (as described here). Below a usage example:

I also wrote some helper functions that might be helpfull: