Friday, September 25, 2009

Global hotkeys for Java applications under Windows OS




Hello all readers.



I would like to present the solution which helps me to realize the working with Global hotkeys in a Java application under Windows OS.


As we remember, Java allows to catch keyboard events for a key or for a key combination, but this works only if the Java application frame or console is active at this moment, but if the user opens or select antoher window, the keyboard events will not reach our Java application. Each application has own event(message) queue and the keyboard events will be sent into this queue unless the application window(console) is active.


To catch global events the program should use special system functions, but Java machine does not provide us this facility by using Java SDK.

So we should develop own solution for managing global hotkeys out of Java sandbox.

My solution is to create a system library (DLL) which can easily calls necessary system functions to listen the global hotkey events. The DLL will be linked up with our Java application with the aid of Java Native Interface (JNI).


The solution which I presented bellow, concerns Windows OS and will not work under Linux.


Let's start!


According to my plan I am going to create the DLL (Dynamic-link library), which will be used by our Java application. The DLL will be initialized by the Java application and the global keyboard events will be managed by the DLL.

The role of Java application is to check the runtime state of the DLL instance periodically. If the hotkey combination was pressed the state of the DLL instance will be changed by setting a special flag.


I used MS Visual Studio 10 and C++ language for developing of the DLL:
on the project page you can find source codes and compiled versions for 32 and 64 bit platforms.
http://code.google.com/p/jkeyboard-hook/


Let's start to  overview my solution from the Java part:

I created the simple testing class for the demonstration of hotkey catching.

We are going to test our keyboard hook on a random hotkey combination like as "CTRL + ALT + H"

You can see that we create GlobalKeyboardHook instance, and specify our hotkey combination with the help of boolean flags and the virtual key value. 
  • KeyEvent.VK_H - Virtual key code for the "H".
  • ALT_Key - if the ALT key is used
  • CTRL_Key - if the CTRL key is used
  • SHIFT_Key - if the SHIFT key is used
  • WIN_Key - if the special "Win" keys are used. These keys have Windows logo.

If the hotkey combination is set, the program can start the hook.

After that the hook will be started, the program waits for the hotkey is pressed to print the "CTRL + ALT + H was pressed" message


com.biletnikov.hotkeys.Main.java:


package com.biletnikov.hotkeys;
import java.awt.event.KeyEvent;
/**
* Testing!
* @author Sergei.Biletnikov
*/
public class Main{

    public static void main(String[] argv) {
        GlobalKeyboardHook hook = new GlobalKeyboardHook();
        // Let me define the following hotkeys: CTRL + ALT + H
        int vitrualKey = KeyEvent.VK_H;
        boolean CTRL_Key = true;
        boolean ALT_Key = true;
        boolean SHIFT_Key = false;
        boolean WIN_Key = false;
        //
        hook.setHotKey(vitrualKey, ALT_Key, CTRL_Key, SHIFT_Key, WIN_Key);
        hook.startHook();
        // waiting for the event
        hook.addGlobalKeyboardListener(new GlobalKeyboardListener() {
            public void onGlobalHotkeysPressed() {
                System.out.println("CTRL + ALT + H was pressed");
            }
        });
        System.out.println("The program waiting for CTRL+ALT+H hotkey...");
    }
}


Nothing special here, it is just a Listener interface.


com.biletnikov.hotkeys.GlobalKeyboardListener.java:


package com.biletnikov.hotkeys;
import java.util.EventListener;
/**
* Hotkeys listener.
* @author Sergei.Biletnikov
*/
public interface GlobalKeyboardListener extends EventListener {
    void onGlobalHotkeysPressed();

}


GlobalKeyboardHook loads DLL that must be placed in the classpath of the Java application, initializes it through the native methods and starts the special thread (DLLStateThread) which checks the state of the DLL instance every 100 ms.

If the hotkey event is occurred the DLLStateThread gets to know about this from the DLL instance by JNI and calls the event listeners.


To link up the DLL with the Java application through JNI, we should generate the header file in C++ which regards to the class that contains native methods (GlobalKeyboardHook).


Compile the java classes and execute: %JAVA_HOME%\bin\javah -jni com.biletnikov.hotkeys.GlobalKeyboardHook to generate the header file: com_biletnikov_hotkeys_GlobalKeyboardHook.h


com.biletnikov.hotkeys.GlobalKeyboardHook.java:


package com.biletnikov.hotkeys;
import java.util.List;
import java.util.ArrayList;
/**
* Global keyboard hook at the Java side.
* @author Sergei.Biletnikov
*/
public class GlobalKeyboardHook {
  
    // ----------- Java Native methods -------------
  
    /**
    * Checks if the hotkeys were pressed.
    * @return true if they were pressed, otherwise false
    */
    public native boolean checkHotKey();
  
    /**
    * Sets the hot key.
    * @param virtualKey Specifies the virtual-key code of the hot key.
    * @param alt Either ALT key must be held down.
    * @param control Either CTRL key must be held down.
    * @param shift Either SHIFT key must be held down.
    * @param win Either WINDOWS key was held down. These keys are labeled with the Microsoft Windows logo.
    * Keyboard shortcuts that involve the WINDOWS key are reserved for use by the operating system.
    * @return If the function succeeds, the return value is TRUE.
    */
    public native boolean setHotKey(int virtualKey, boolean alt, boolean control, boolean shift, boolean win);
  
    /**
    * Resets the installed hotkeys.
    */
    public native void resetHotKey();
    // -------------------------------------
  
    // GlobalKeyboardHook.dll
    private static final String KEYBOARD_HOOOK_DLL_NAME = "GlobalKeyboardHook";
  
    /**
    * For stopping
    */
    private boolean stopFlag;
  
    // -------- Java listeners --------
    private List listeners = new ArrayList();
  
    /**
    * Constructor.
    */
    public GlobalKeyboardHook() {
        // load KeyboardHookDispatcher.dll in the classpath
        System.loadLibrary(KEYBOARD_HOOOK_DLL_NAME);
        System.out.println(KEYBOARD_HOOOK_DLL_NAME + ".dll was loaded");
        stopFlag = false;
    }
  
    public void addGlobalKeyboardListener(GlobalKeyboardListener listener) {
        listeners.add(listener);
    }
  
    public void removeGlobalKeyboardListener(GlobalKeyboardListener listener) {
        listeners.remove(listener);
    }
  
    /**
    * Start the hook. Create and run DLLStateThread thread, for checking the DLL status.
    */
    public void startHook() {
        stopFlag = false;
        DLLStateThread currentWorker = new DLLStateThread();
        Thread statusThread = new Thread(currentWorker);
        statusThread.start();
    }
  
    /**
    * Finish the current KeyboardThreadWorker instance.
    */
    public void stopHook() {
        stopFlag = true;
    }
  
    /**
    * Sends the event notification to all listeners.
    */
    private void fireHotkeysEvent() {
        for (GlobalKeyboardListener listener : listeners) {
            listener.onGlobalHotkeysPressed();
        }
    }
  
    /**
    * This class is base for the thread, which monitors DLL status.
    */
    private class DLLStateThread implements Runnable {
      
        public void run() {
            for(;;) {
                boolean hotKeyPressed = checkHotKey();
                if (hotKeyPressed) {
                    // hot key was pressed, send the event to all listeners
                    fireHotkeysEvent();
                }
                try {
                    Thread.sleep(100); //every 100 ms check the DLL status.
                    // work unless stopFlag == false
                    if (stopFlag) {
                        resetHotKey();
                        break;
                    }
                    } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


com_biletnikov_hotkeys_GlobalKeyboardHook.h:


/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_biletnikov_hotkeys_GlobalKeyboardHook */

#ifndef _Included_com_biletnikov_hotkeys_GlobalKeyboardHook
#define _Included_com_biletnikov_hotkeys_GlobalKeyboardHook
#ifdef __cplusplus
extern "C" {
    #endif
    /*
    * Class: com_biletnikov_hotkeys_GlobalKeyboardHook
    * Method: checkHotKey
    * Signature: ()Z
    */
    JNIEXPORT jboolean JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_checkHotKey
    (JNIEnv *, jobject);

    /*
    * Class: com_biletnikov_hotkeys_GlobalKeyboardHook
    * Method: setHotKey
    * Signature: (IZZZZ)Z
    */
    JNIEXPORT jboolean JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_setHotKey
    (JNIEnv *, jobject, jint, jboolean, jboolean, jboolean, jboolean);

    /*
    * Class: com_biletnikov_hotkeys_GlobalKeyboardHook
    * Method: resetHotKey
    * Signature: ()V
    */
    JNIEXPORT void JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_resetHotKey
    (JNIEnv *, jobject);

    #ifdef __cplusplus
}
#endif
#endif



Open you IDE for building the DLL. In my case it is Visual Studio 6.0.

Create a DLL project with Microsoft Visual Studio (or other IDE).

Add com_biletnikov_hotkeys_GlobalKeyboardHook.h and all C++ header files which you can find in the %JDK_HOME%\include\ directory.


Create the main C++ file: GlobalKeyboardHook.cpp, with the following content:


GlobalKeyboardHook.cpp:

/*
* Global Keyboard hook
*
*
* JNI Interface for setting a Keyboard Hook and monitoring
* it Java-side
*
*/

#include
#include "windows.h"
#include "Winuser.h"
#include "jni.h"
#include "com_biletnikov_hotkeys_GlobalKeyboardHook.h"

#pragma data_seg(".HOOKDATA") //Shared data among all instances.

static HHOOK hotKeyHook = NULL;
static HANDLE g_hModule = NULL;

const int HOT_KEY_ID = 0xBBBC;
static int hotKeyRegisterStatus = 0;

// Hot key settings
static int keyModifiers = 0;
static int virtualKey = 0;
//
static int hotKeyPressedFlag = 0;

HANDLE messageThreadEvent = NULL;
HANDLE messageThread = NULL;
DWORD messageThreadID;

#pragma data_seg()
#pragma comment(linker, "/SECTION:.HOOKDATA,RWS")
/*
* Class: de_alfah_popup_jni_KeyboardHookThread
* Method: checkHotKey
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_checkHotKey
(JNIEnv *env, jobject obj)
{
    int pressedFlag = hotKeyPressedFlag;
    hotKeyPressedFlag = 0;
    return pressedFlag != 0 ? JNI_TRUE : JNI_FALSE;
}

DWORD WINAPI HotKeyListener(LPVOID lpParameter)
{
    hotKeyRegisterStatus = RegisterHotKey(NULL, HOT_KEY_ID, keyModifiers, virtualKey);

    SetEvent(messageThreadEvent);
    if (hotKeyRegisterStatus !=0)
    {
        MSG msg = {0};
        while (GetMessage(&msg, NULL, 0, 0) != 0)
        {
            switch (msg.message)
            {
                case WM_HOTKEY:
                hotKeyPressedFlag = 1;
                break;
            }
        }
    }
    return hotKeyRegisterStatus;;
}

// Reset hot keys by terminating the message thread
static void resetHotKey()
{
    UnregisterHotKey(NULL, HOT_KEY_ID);
    if (messageThread != NULL)
    {
        PostThreadMessage((DWORD) messageThreadID, (UINT) WM_QUIT, 0, 0);
        // TerminateThread (messageThread, 0);
        CloseHandle(messageThread);
        messageThread = NULL;
        messageThreadID = NULL;
    }
    if (messageThreadEvent != NULL) {
        CloseHandle(messageThreadEvent);
        messageThreadEvent = NULL;
    }
    hotKeyRegisterStatus = 0;
}

// Sets new hot key
static int setNewHotKey()
{
    messageThreadEvent = CreateEvent( NULL, TRUE, TRUE, NULL );
    ResetEvent( messageThreadEvent );

    messageThread = CreateThread(NULL, 0, HotKeyListener, 0,0,&messageThreadID);

    WaitForSingleObject( messageThreadEvent, INFINITE );

    return hotKeyRegisterStatus;
}

/*
* Class: de_alfah_popup_jni_KeyboardHookThread
* Method: setHotKey
* Signature: (IZZZZ)Z
*/
JNIEXPORT jboolean JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_setHotKey
(JNIEnv *, jobject, jint vk, jboolean altKey, jboolean ctrlKey, jboolean shiftKey, jboolean winKey)
{
    // define modifiers
    int modifiers = 0;

    if (altKey == JNI_TRUE)
    {
        modifiers = MOD_ALT;
        } if (ctrlKey == JNI_TRUE) {
        modifiers = modifiers | MOD_CONTROL;
        } if (shiftKey == JNI_TRUE) {
        modifiers = modifiers | MOD_SHIFT;
        } if (winKey == JNI_TRUE) {
        modifiers = modifiers | MOD_WIN;
    }
    // set key setings
    keyModifiers = modifiers;
    virtualKey = vk;

    // reset previous hot key
    resetHotKey();
    //
    int status = setNewHotKey();

    return status == 0 ? JNI_FALSE : JNI_TRUE;
}

/*
* Class: de_alfah_popup_jni_KeyboardHookThread
* Method: resetHotKey
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_biletnikov_hotkeys_GlobalKeyboardHook_resetHotKey
(JNIEnv * env, jobject)
{
    resetHotKey();
}

// The main DLL
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
    switch(ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        g_hModule = hModule;
        return TRUE;
        case DLL_PROCESS_DETACH:
        resetHotKey();
        return TRUE;
    }
    return TRUE;
}


Compile the DLL.

The GlobalKeyboardHook.dll will appear if all things are fine.


I found two ways how to catch global keyboard events:

  • Using SetWindowsHookEx it is universal hook for many kinds of the events, but it is not handy for catching more than 2 keys combinations
  • RegisterHotKey is a nice function for catching hotkeys.


The DLL use RegisterHotKey function to register a hotkey combination.

The one important aspect is this function sends messages to the message queue of the thread that performed the hotkey registration, therefore we must have a separate thread which fulfills hotkey assigning and listens the message queue.

For each new hotkey combination, the DLL creates the thread which assigns the required hotkey combination for RegisterHotKey and waits for the WM_HOTKEY message.


The current hook DLL was designed to process one hotkey combination. If we set a new combination, the previous thread will be ended and a new one will be created.

Have nice hotkeys :)