Search code examples
javajna

Key listener written in Java JNA. Cannot stop the thread


I use the following code to listen to global key events:

Win32HookManager.java

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HMODULE;
import com.sun.jna.platform.win32.WinDef.LRESULT;
import com.sun.jna.platform.win32.WinDef.WPARAM;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.platform.win32.WinUser.HHOOK;
import com.sun.jna.platform.win32.WinUser.KBDLLHOOKSTRUCT;
import com.sun.jna.platform.win32.WinUser.LowLevelKeyboardProc;
import com.sun.jna.platform.win32.WinUser.MSG;


import java.awt.event.KeyEvent;

public class Win32HookManager {
    private static HHOOK keyboardHook;

    public static boolean installKeyboardHook(final NativeKeyboardListener listener) {
        final User32  lib  = User32.INSTANCE;
        final HMODULE hMod = Kernel32.INSTANCE.GetModuleHandle(null);

        final LowLevelKeyboardProc keyboardHookProc = new LowLevelKeyboardProc() {
            @Override
            public LRESULT callback(int nCode, WPARAM wParam, KBDLLHOOKSTRUCT info) {
                NativeKeyboardEvent ev = null;
                long                ti = System.currentTimeMillis();
                boolean             nh = true;

                if (nCode >= 0) {
                    switch (wParam.intValue()) {
                        case WinUser.WM_KEYDOWN:
                        case WinUser.WM_SYSKEYDOWN:
                            ev = new NativeKeyboardEvent(KeyEvent.KEY_PRESSED, ti, 0, info.vkCode);
                            nh = listener.keyPressed(ev);
                            break;

                        case WinUser.WM_KEYUP:
                        case WinUser.WM_SYSKEYUP:
                            ev = new NativeKeyboardEvent(KeyEvent.KEY_RELEASED, ti, 0, info.vkCode);
                            nh = listener.keyReleased(ev);
                            break;
                    }
                }

                if(nh) {
                    return lib.CallNextHookEx(keyboardHook, nCode, wParam, info.getPointer());
                }
                return new LRESULT(1);
            }
        };

        new Thread() {
            @Override
            public void run() {
                keyboardHook = lib.SetWindowsHookEx(WinUser.WH_KEYBOARD_LL, keyboardHookProc, hMod, 0);
                msgLoop();
                lib.UnhookWindowsHookEx(keyboardHook);
            }
        }.start();

        return keyboardHook != null;
    }

    public static boolean uninstallKeyboardHook() {
        if(keyboardHook != null) {
            return User32.INSTANCE.UnhookWindowsHookEx(keyboardHook);
        }

        return false;
    }

    private static void msgLoop()
    {
        final User32 lib = User32.INSTANCE;

        int result;
        MSG msg = new MSG();
        while ((result = lib.GetMessage(msg, null, 0, 0)) != 0) {
            if (result == -1) {
                System.err.println("error in get message");
                break;
            }
            else {
                System.err.println("got message");
                lib.TranslateMessage(msg);
                lib.DispatchMessage(msg);
            }
        }
    }
}

NativeKeyboardListener

public interface NativeKeyboardListener {
    public boolean keyPressed (NativeKeyboardEvent e);
    public boolean keyReleased(NativeKeyboardEvent e);
}

NativeKeyboardEvent

public class NativeKeyboardEvent {
    private int  id;
    private int  keyCode;

    public NativeKeyboardEvent(int id, long when, int modifiers, int keyCode) {
        this.id        = id;
        this.keyCode   = keyCode;
    }

    public int getId() {
        return id;
    }

    public int getKeyCode() {
        return keyCode;
    }
}

Unfortunately, it doesn't work as I expected, i.e. it detects when a key is pressed/released, but it cannot finish the thread started by installKeyboardHook() method because of GetMessage() in msgLoop method. Yes, I can stop listening to key events, but I cannot stop the thread. However, it seems that GetMessage() is needed in this code. Do you see any workaround for this problem?

Thanks!


Solution

  • As @SLaks suggests, you need some sort of flag to indicate whether or not the message loop should continue to run. You can then use PeekMessage (JNA Doc) which, like GetMessage, retrieves a message from the queue, but is not a blocking operation. Your message loop should then be changed to something like this:

    while (!shouldQuit) {
        while ((result = lib.PeekMessage(msg, null, 0, 0, 1)) != 0) {
            // ...
        }
    }