Gepard
Gepard

Reputation: 67

Swing application Thread locked by JNA

I have Swing application on Windows 10. Also I added Low Level Keyboard hook which should intercept keyboard events and remap for example 'z' to 's' button.

This is needed to hook keyboard events outside my java application. I have implemented it using JNA version 5.6.0 and jna-platform version 5.6.0. It works fine but not inside my Swing application.

My problem is when the hook is ON, swing application a kind of locked. I can not press to any Jbutton and even close Jframe at all.

My guess is that it has something to do with threads, but I'm very weak at threads and multithreading.

Reproducible example.

TestFrame class:

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class TestFrame extends JFrame {

    public static void main(String [] args) {
        TestFrame frame = new TestFrame();

        JTextField textField=new JTextField();
        textField.setBounds(50,50, 150,20);

        JButton button=new JButton("Click Here");
        button.setBounds(50,100,95,30);

        button.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e){
                ReMapper reMapper = new ReMapper();
                reMapper.reMapOn();
                textField.setText("Is my frame locked?");
            }
        });
        frame.add(button);
        frame.add(textField);
        frame.setSize(400,400);
        frame.setLayout(null);
        frame.setVisible(true);
    }
}

ReMapper class:

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.*;

public class ReMapper {

    private static WinUser.HHOOK hHook;
    final User32 user32Library = User32.INSTANCE;
    WinDef.HMODULE hMod = Kernel32.INSTANCE.GetModuleHandle(null);
    static WinUser.INPUT input = new WinUser.INPUT();
    
    public void reMapOn() {
        WinUser.LowLevelKeyboardProc keyboardHook = new WinUser.LowLevelKeyboardProc() {

            @Override
            public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, WinUser.KBDLLHOOKSTRUCT kbDllHookStruct) {
                if (nCode >= 0) {
                    if (wParam.intValue() == WinUser.WM_KEYDOWN) {
                        if (kbDllHookStruct.vkCode == 90) { // 90 is key code = z
                            sendKey(83);            // 83 is key code = s
                            return new WinDef.LRESULT(1);
                        }
                    }
                }
                Pointer ptr = kbDllHookStruct.getPointer();
                long peer = Pointer.nativeValue(ptr);
                return user32Library.CallNextHookEx(hHook, nCode, wParam, new WinDef.LPARAM(peer));
            }
        };

        hHook = user32Library.SetWindowsHookEx(WinUser.WH_KEYBOARD_LL, keyboardHook, hMod, 0);

        int result;
        WinUser.MSG msg = new WinUser.MSG();
        while ((result = user32Library.GetMessage(msg, null, 0, 0)) != 0) {
            if (result == -1) {
                break;
            } else {
                user32Library.TranslateMessage(msg);
                user32Library.DispatchMessage(msg);
            }
        }
    }

    static void sendKey(int keyCode) {
        input.type = new WinDef.DWORD(WinUser.INPUT.INPUT_KEYBOARD);
        input.input.setType("ki"); // Because setting INPUT_INPUT_KEYBOARD is not enough: https://groups.google.com/d/msg/jna-users/NDBGwC1VZbU/cjYCQ1CjBwAJ
        input.input.ki.wScan = new WinDef.WORD(0);
        input.input.ki.time = new WinDef.DWORD(0);
        input.input.ki.dwExtraInfo = new BaseTSD.ULONG_PTR(0);
// Press
        input.input.ki.wVk = new WinDef.WORD(keyCode); // 0x41
        input.input.ki.dwFlags = new WinDef.DWORD(0);  // keydown

        User32.INSTANCE.SendInput(new WinDef.DWORD(1), (WinUser.INPUT[]) input.toArray(1), input.size());

// Release
        input.input.ki.wVk = new WinDef.WORD(keyCode); // 0x41
        input.input.ki.dwFlags = new WinDef.DWORD(2);  // keyup

        User32.INSTANCE.SendInput(new WinDef.DWORD(1), (WinUser.INPUT[]) input.toArray(1), input.size());
    }
}

Here is the screen with locked Jframe after clicking 'Click Here' button:

enter image description here

ReMapper class works fine separately from Swing application.

reMapOn() allows to remap 'z' to 's'. But I need it to work inside my Swing app and not block it..

Does anyone know what the problem might be and how to fix it?

Upvotes: 1

Views: 139

Answers (1)

David Kroukamp
David Kroukamp

Reputation: 36423

Looking at your reMapOn code, it has a while loop and that points to the fact that it could run indefinitely and block the applications UI.

What you need to do is simply in your addActionListener method call the reMapOn method on its own thread. This can be done either using a simple Thread or Swing Worker:

Swing Worker example (preferred solution as you can override done and manipulate swing components if needed in that method when the remapper had ended):

new SwingWorker<Void, Void>() {
    @Override
    protected Void doInBackground() throws Exception {
        ReMapper reMapper = new ReMapper();
        reMapper.reMapOn();
        return null;
    }
    
}.execute();
textField.setText("Is my frame locked?");

Thread example:

new Thread(() -> {
    ReMapper reMapper = new ReMapper();
    reMapper.reMapOn();
}).start();
textField.setText("Is my frame locked?");

Some other points are:

  1. Don't use a null/AbsoluteLayout rather use an appropriate LayoutManager
  2. Don't call setBounds() or setSize() on components, if you use a correct layout manager this will be handled for you
  3. Call JFrame#pack() before setting the frame to visible when using a LayoutManager
  4. Dont extend the JFrameclass unnecessarily
  5. All Swing components should be called on the EDT via SwingUtilities.invokeLater

Upvotes: 2

Related Questions