Search code examples
javawinapiapijava-native-interfacejna

GetAsyncKeyState and VirtualKeys/special characters using JNA (JAVA)


I am working on a two-way private chat that will work in a full screen game.

This is required to let the user to type into a semi-transparent textbox at the top of the screen even when it doesn't have focus.

Using the following code, I can detect ALL physical keys, but have a tough time with virtual keys.

SHIFT is detected.

2 is detected.

However Shift + 2 are detected both as separate keys (Even though [SHIFT+2] gives @ on my keyboard). IE: The program outputs both SHIFT, and 2, but not what they produce: @.

The problem is, how will I convert to a character depending on the keyboard? For example:

  • On a UK Keyboard, SHIFT+2 will give me " (quotes).
  • On a US keyboard, SHIFT +2 will give me @.

How can I convert to a specific character depending on the keyboard?

Here is the code so far:

static interface User32 extends Library {
    public static User32 INSTANCE = (User32) Native.loadLibrary("User32", User32.class);

    short GetAsyncKeyState(int key);
    short GetKeyState(int key);

    IntByReference GetKeyboardLayout(int dwLayout);
    int MapVirtualKeyExW (int uCode, int nMapType, IntByReference dwhkl);

    boolean GetKeyboardState(byte[] lpKeyState);

    int ToUnicodeEx(int wVirtKey, int wScanCode, byte[] lpKeyState, char[] pwszBuff, int cchBuff, int wFlags, IntByReference dwhkl);

}



public static void main(String[] args)  {   
    long currTime = System.currentTimeMillis();

    while (System.currentTimeMillis() < currTime + 20000)
    {
        for (int key = 1; key < 256; key++)
            {
                if (isKeyPressed(key)) 
                    getKeyType(key);
            }
    }
}



private static boolean isKeyPressed(int key)
{
    return User32.INSTANCE.GetAsyncKeyState(key) == -32767;
}



private static void getKeyType(int key)
{

    boolean isDownShift = (User32.INSTANCE.GetKeyState(VK_SHIFT) & 0x80) == 0x80;
    boolean isDownCapsLock = (User32.INSTANCE.GetKeyState(VK_CAPS)) != 0;


    byte[] keystate = new byte[256];
    User32.INSTANCE.GetKeyboardState(keystate); 


    IntByReference keyblayoutID = User32.INSTANCE.GetKeyboardLayout(0);
    int ScanCode  = User32.INSTANCE.MapVirtualKeyExW(key, MAPVK_VK_TO_VSC, keyblayoutID);






    char[] buff = new char[10];

    int bufflen = buff.length;
    int ret = User32.INSTANCE.ToUnicodeEx(key, ScanCode, keystate, buff, bufflen, 0, keyblayoutID);


    switch (ret)
    {
        case -1: 
            System.out.println("Error");
        break;

        case 0:  // no translation

        break;

        default: 
        System.out.println("output=" + String.valueOf(buff).substring(0, ret));
    }




}

It works fine and outputs the keys pressed, but doesn't work with Shift + combinations. I realize that I could do a "Switch" and change Shift+3 to "£", but this will not work with different keyboards.


Solution

  • I got it. After many, many, many hours of searching, I managed to create a method that converts the combination to what it should be on the current keyboard layout. It doesn't deal with dead-keys (such as accents), but it catches all the [SHIFT+Combinations] that I need it to catch.

    To use it, call it as follows:

    getCharacter(int vkCode, boolean shiftKeyPressed);
    

    So, watch this magic. If I want to get what SHIFT+3 will give me on my keyboard (£), I use:

    getCharacter(KeyEvent.VK_3, true);
    

    Here is the code:

    public static char getCharacter(int vkCode, boolean shiftKeyPressed)
    {
    
        byte[] keyStates = new byte[256]; //Create a keyboard map of 256 keys
    
        if (shiftKeyPressed)
        {
            keyStates[16]=-127; //Emulate the shift key being held down
            keyStates[160]=-128; //This needs to be set as well
        }
    
        IntByReference keyblayoutID = User32.INSTANCE.GetKeyboardLayout(0); //Load local keyboard layout
    
        int ScanCode  = User32.INSTANCE.MapVirtualKeyExW(vkCode, MAPVK_VK_TO_VSC, keyblayoutID); //Get the scancode
    
        char[] buff = new char[1];
    
        int ret = User32.INSTANCE.ToUnicodeEx(vkCode, ScanCode, keyStates, buff, 1, 0, _currentInputLocaleIdentifier);
    
        switch (ret)
        {
        case -1: //Error
            return (char) -1;
    
        case 0:  //No Translation
            return (char) 0;
    
        default: //Returning key...
            return buff[0];
        }
    }
    

    Here are the declarations:

    final static int MAPVK_VK_TO_VSC = 0;
    static IntByReference _currentInputLocaleIdentifier; 
    
    static interface User32 extends Library {
    
        public static User32 INSTANCE = (User32) Native.loadLibrary("User32", User32.class);
    
    
        IntByReference GetKeyboardLayout(int dwLayout);
        int MapVirtualKeyExW (int uCode, int nMapType, IntByReference dwhkl);
    
        boolean GetKeyboardState(byte[] lpKeyState);
    
        int ToUnicodeEx(int wVirtKey, int wScanCode, byte[] lpKeyState, char[] pwszBuff, int cchBuff, int wFlags, IntByReference dwhkl);
    
    }
    

    A big thank you to BrendanMcK, who helped me to get to this solution.