Use MapVirtualKeyA with Shift and Ctrl Alt

Viewed 1031

I have found this page for a function that converts a given keycode in to the corresponding charcater, depending on the system language and keyboard layout. I tested it (in PowerPoint using VBA) and it works, except I don't know how to tell the function that Shift or Ctrl + Alt are being pressed and therefore the function should then return a different result. Say for instance: The keycode 51 corresponds to the number 3, when I pass it to the function, it returns 3. But what if the user is pressing the Shift key? It should return # (or a different character, depending on the keyboard layout, etc.)

So, I know how to check if Shift or Ctrl + Alt are being pressed, but what I don't know is how to tell the function that the keys are being pressed.

I put the following code in a module:

Public Declare PtrSafe Function MapVirtualKeyA Lib "user32" (ByVal uCode As Long, ByVal uMapType As Long) As Integer

And then in the slide's code I put:

Sub test()

    MsgBox Chr(MapVirtualKeyA(vbKey3, 2)) ' always returns 3, even when pressing shift or ctrl + alt

End Sub

I want to know what I have to change in my code so that function knows that Shift or Ctrl + Alt are being pressed.

Thanks in advance.

3 Answers

To understand this answer to your question, we need to first look at how keyboard input works.

Microsoft have a whole section on this:

When a key is pressed on a keyboard, a scan code is generated. Scan codes are device-dependent.

Next:

The keyboard device driver receives scan codes from the keyboard

and turns them into virtual keys:

a device-independent value defined by the system that identifies the purpose of a key.

A virtual key represents the keys on a keyboard, and not what characters they can represent. So, for example 3 is a virtual key, but it's shifted value (£ in my case), isn't, and depends on the keyboard in use.

These are stored in the device-dependent KeyboardLayout. The basic function calls such as MapVirualKey use a default keyboard layout (i.e. the one you are using now). If you want to use different layouts, you can get a list by calling:

GetKeyboardLayoutList

So a virtual key and any modifiers are sent to the keyboard layout and this returns the appropriate character, which is then rendered to the screen, etc...

So your question becomes, 'How can we find the Unicode/ASCII value of myKey + shift?'. There isn't any particularly easy answer, for example on my computer 'shift 3 (£)' with ASCII value 163, but 'shift 4 ($)' has an ASCII value of 36!

These examples use Excel, not Powerpoint.

Let's look at MapVirtualKey` first. I have not used a general keyboard layout.

In a module, add the following declaration:

' https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapvirtualkeya
Public Declare PtrSafe Function MapVirtualKeyA Lib "user32" ( _
        ByVal uCode As Long, _
        ByVal uMapType As Long) As Integer

and add the following subroutine to Button1:

Sub Button1_Click()

Cells.Clear

' MapVirtualKeyA
Cells(1, 1) = "MapVirtualKeyA"
Cells(1, 1).Font.Bold = 1

char = "3"
Cells(2, 1) = "char: " & char
Cells(2, 2) = MapVirtualKeyA(Asc(char), 2)
Cells(2, 3) = Chr(MapVirtualKeyA(Asc(char), 2))

char = "£"
Cells(3, 1) = "char: " & char
Cells(3, 2) = MapVirtualKeyA(Asc(char), 2)
Cells(3, 3) = Chr(MapVirtualKeyA(Asc(char), 2)) ' doesn't exist

The first call works fine, the second doesn't recognise £ as a virtual key (it isn't), and so returns 0.

So this isn't much use at determining what 'shift 3' should return.

We can retrieve the 'shift' state from £, by using the VkKeyScan API function.

Declare the function in a module:

' https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscana
Public Declare Function VkKeyScan Lib "user32" Alias "VkKeyScanA" ( _
        ByVal cChar As Byte) As Integer

and add this code to a button:

'VkKeyScan
Cells(5, 1) = "VkKeyScan"
Cells(5, 1).Font.Bold = 1

char = "3"
keyScan = VkKeyScan(Asc(char))
shift = keyScan And &H100
ctrl = keyScan And &H200
vkKey = keyScan And &HFF

Cells(6, 1) = "char: " & char
Cells(6, 2) = "shift: " & shift
Cells(6, 3) = "ctrl: " & ctrl
Cells(6, 4) = keyScan
Cells(6, 5) = ChrW(keyScan)
Cells(6, 6) = vkKey
Cells(6, 7) = Chr(vkKey)

char = "£"
keyScan = VkKeyScan(Asc(char))
shift = keyScan And &H100
ctrl = keyScan And &H200
vkKey = keyScan And &HFF

Cells(7, 1) = "char: " & char
Cells(7, 2) = "shift: " & shift
Cells(7, 3) = "ctrl: " & ctrl
Cells(7, 4) = keyScan
Cells(7, 5) = ChrW(keyScan) ' wrong character
Cells(7, 6) = vkKey
Cells(7, 7) = Chr(vkKey)

This is more informative, but still doesn't give us the answer.

There are two solutions from here, both are pretty much the same:

SendInput is the official Win32API way to do things, but involves fairly long code, and is neatly wrapped up by the VBA SendKeys function.

SendKeys sends text output to the currently active cursor.

Cells(10, 1).Select
SendKeys ("3+3~")
While Cells(10, 1) = ""
DoEvents
Wend
Cells(10, 2) = Asc(Mid(Cells(10, 1), 1, 1))
Cells(10, 3) = Asc(Mid(Cells(10, 1), 2, 1))
End Sub

Cells(10, 1).Select places the cursor where we want it.

+ is used to simulate 'shift', and ~ simulates 'enter'.

Wait while the processor catches up.., and we have the ASCII value for '£'!

You could use ToAscii API.

Option Explicit

Private Declare PtrSafe Function ToAscii Lib "user32" (ByVal uVirtKey As Long, ByVal uScanCode As Long, lpbKeyState As Byte, lpChar As LongPtr, ByVal uFlags As Long) As Long
Private Declare PtrSafe Function MapVirtualKeyA Lib "user32" (ByVal uCode As Long, ByVal uMapType As Long) As Long

Sub Test()
    Debug.Print GetCharacterFromVK(uVirtKey:=vbKey3, shiftDown:=True)
End Sub

Function GetCharacterFromVK(uVirtKey As Long _
    , Optional shiftDown As Boolean = False _
    , Optional ctrlDown As Boolean = False _
    , Optional altDown As Boolean = False _
) As String
    'https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
    Const VK_SHIFT = &H10   'SHIFT key
    Const VK_CONTROL = &H11 'CTRL key
    Const VK_MENU = &H12    'ALT key
    '
    Dim keyState(0 To 255) As Byte
    Dim buffer As String: buffer = Space$(2)
    '
    'Set Key States by setting the high-order bit of the byte
    If shiftDown Then keyState(VK_SHIFT) = &H80
    If ctrlDown Then keyState(VK_CONTROL) = &H80
    If altDown Then keyState(VK_MENU) = &H80
    '
    'Populate buffer
    Select Case ToAscii(uVirtKey, MapVirtualKeyA(uVirtKey, 0), keyState(0), ByVal StrPtr(buffer), 0)
    Case 0
        'The specified virtual key has no translation for the current state of the keyboard
        GetCharacterFromVK = vbNullString
    Case 1
        'One character was copied to the buffer
        GetCharacterFromVK = Left$(buffer, 1)
    Case 2
        'Two characters were copied to the buffer
        GetCharacterFromVK = buffer
    End Select
End Function

This should help (though if you're using Office in a 64-bit version, it'll need to be modified to include 64-bit compatible declarations).

(In declarations)
Public Const VK_SHIFT = &H10
Public Const VK_CONTROL = &H11
Public Const VK_MENU = &H12
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer

Public Function GetAltState() As Boolean
    Call GetAsyncKeyState(VK_MENU)
    If GetAsyncKeyState(VK_MENU) <> 0 Then GetAltState = True
End Function

Public Function GetCtrlState() As Boolean
    Call GetAsyncKeyState(VK_CONTROL)
    If GetAsyncKeyState(VK_CONTROL) <> 0 Then GetCtrlState = True
End Function

Public Function GetShiftState() As Boolean
    Call GetAsyncKeyState(VK_SHIFT)
    If GetAsyncKeyState(VK_SHIFT) <> 0 Then GetShiftState = True
End Function
Related