gmatt
July 19th, 2008, 11:57 PM
This tutorial is for Java devs who want to be able to capture key presses globally under linux (specifically ubunutu.) This is of course OS dependent as Java does not allow for such a thing without the use of the JNI (Java Native Interface.)
This tutorial is as self-contained as possible; however, if you would like more information about JNI a good place to start is the documentation at http://java.sun.com/developer/onlineTraining/Programming/JDCBook/jni.html .
The Problem
The problem is that you would like your Java application to detect keys pressed by the user even if your Java application does not have focus. This is not a trivial problem, as I have learned. For security reasons or convenience, the developers of Java have omitted to give Java developers a painless way of capturing system-wide events such as key presses.
The Solution
Upon further investigation, most operating systems do offer developers a means of capturing such events. On windows, developers can use hooks to listen for global (input) events. On linux, developers can use the X11 library to capture gloabl input events. Unfortunately, both of these are not directly accessible through Java code (windows hooks might be.)
Under linux, a developer can write his/her own code using C/C++ X11 libraries. This, of course, is only part of the solution, since then your Java application must be able to work in harmony with the C/C++ code. Fortunately, JNI allows just that.
The Dirty Details
Before we do anything, make sure that you have X11-dev libraries installed with
sudo apt-get install libx11-dev
Let's start with the implementation. First we write a self-contained Java class as follows:
class KeyGrabber {
static {
System.loadLibrary("KeyGrabber");
}
private native void listen();
public static void fire_key_event(){
System.out.println("key pressed (java code)");
}
public static void main(String[] args) {
new KeyGrabber().listen();
}
}
In the Java code the listen() function is labeled native which tells the Java compiler that it is implemented in C/C++ code that it must link. The listen() function will be the X11 code required to listen for global key presses.
The fire_key_event() function is completely implemented in Java. This function will be called from the native function listen() whenever the X11 code detects a global key press.
Next we compile this Java code with:
javac KeyGrabber.java.
To implement the listen() function in C/C++ we must now use JNI. We start by
javah -jni KeyGrabber
which will generate a header file called KeyGrabber.h that looks like
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class KeyGrabber */
#ifndef _Included_KeyGrabber
#define _Included_KeyGrabber
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: KeyGrabber
* Method: listen
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Since we only had one native function in the Java code we only have one corresponding function prototype in the JNI header, namely:
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);
We must implement this function according to this prototype. We can then implement the X11 code necessary to detect global key presses in KeyGrabber.c as follows.
#include <X11/Xlib.h>
#include"KeyGrabber.h"
#include<stdio.h>
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *env, jobject obj){
printf("starting to listen to key F3 (native code)\n");
jclass cls = (*env)->FindClass(env, "KeyGrabber");
if(cls == NULL){
printf("cannot find class KeyGrabber\n") ;
exit(-1);
}
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" );
if(mid ==0){
printf("cannot find method fun\n");
exit(-1);
}
Window root;
XEvent ev;
Display * dpy = XOpenDisplay(0);
if(!dpy) return 1;
root = DefaultRootWindow(dpy);
char * key_string = "F3";
KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);
for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}
printf("leaving c code\n");
return;
}
This code listens for any key press of F3 with any modifier key, and when it detects such a key, calls the Java function "fire_key_event".
Here is a detailed break down of the sections of the code:
jclass cls = (*env)->FindClass(env, "KeyGrabber");
The jclass class is defined in jni.h which is included when you include "KeyGrabber.h". The env pointer that is passed to every native function is essentially pointing to the JVM. You can manipulate anything that the JVM can manipulate.
Our immediate goal is to be able to call the fire_key_event method in KeyGrabber class whenever the X11 code detects a global key press. The first order of business is to get a handle on the KeyGrabber class, which is what this line does.
The next order of business is to get a handle on the fire_key_event method in the KeyGrabber class. That is what
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" ); does.
The first three parameters passed to GetStaticMethodID are clear: pointer to the environment (JVM), the class with the method, the name of the method. The last parameter is the signature of the function (this of course is required because of method overloading.) To get the signature of the desired method use the following command:
javap -s KeyGrabber
which for this example gives:
Compiled from "KeyGrabber.java"
class KeyGrabber extends java.lang.Object{
KeyGrabber();
Signature: ()V
public static void fire_key_event();
Signature: ()V
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
static {};
Signature: ()V
}
and then you can copy and paste the signature straight from terminal ;).
The next bit of code
Window root;
XEvent ev;
Display * dpy = XOpenDisplay(0);
if(!dpy) return 1;
root = DefaultRootWindow(dpy);
char * key_string = "F3";
KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);
is the X11 needed to capture global key presses (in this case only key presses of F3 with any modifier.) I won't go into too much detail, but essentially the XGrabKey function call tells X11 that your code is interested in capturing key press of the F3 key with any modifier on display 0 in the root window (meaning globally.) So the following code
for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}
is an infinite loop that listens to all the registered events (in this case only F3 key presses.) If the event type is a KeyPress and the keycode is F3 then the C code will use the env pointer to call the static void method fire_key_event in the KeyGrabber class.
And its that straightforward (hopefully.)
The last order of business is to compile everything and get it running.
To compile the C code, it is a bit tricky. Provided you have a valid Java dev installation you can compile it with:
gcc --shared -o libKeyGrabber.so -I<path to java>/include -I<path to java>/include/linux -lX11 KeyGrabber.c <path to java>/jre/lib/i386/server/libjvm.so
Then to run the Java you must make sure that Java can link to the libKeyGrabber.so library that you have just compiled, so use the following command
java -Djava.library.path=. KeyGrabber
and everything should work! (eh hopefully.)
I've probably missed a lot of things so I hope you won't hesitate to post any questions and comments!
This tutorial is as self-contained as possible; however, if you would like more information about JNI a good place to start is the documentation at http://java.sun.com/developer/onlineTraining/Programming/JDCBook/jni.html .
The Problem
The problem is that you would like your Java application to detect keys pressed by the user even if your Java application does not have focus. This is not a trivial problem, as I have learned. For security reasons or convenience, the developers of Java have omitted to give Java developers a painless way of capturing system-wide events such as key presses.
The Solution
Upon further investigation, most operating systems do offer developers a means of capturing such events. On windows, developers can use hooks to listen for global (input) events. On linux, developers can use the X11 library to capture gloabl input events. Unfortunately, both of these are not directly accessible through Java code (windows hooks might be.)
Under linux, a developer can write his/her own code using C/C++ X11 libraries. This, of course, is only part of the solution, since then your Java application must be able to work in harmony with the C/C++ code. Fortunately, JNI allows just that.
The Dirty Details
Before we do anything, make sure that you have X11-dev libraries installed with
sudo apt-get install libx11-dev
Let's start with the implementation. First we write a self-contained Java class as follows:
class KeyGrabber {
static {
System.loadLibrary("KeyGrabber");
}
private native void listen();
public static void fire_key_event(){
System.out.println("key pressed (java code)");
}
public static void main(String[] args) {
new KeyGrabber().listen();
}
}
In the Java code the listen() function is labeled native which tells the Java compiler that it is implemented in C/C++ code that it must link. The listen() function will be the X11 code required to listen for global key presses.
The fire_key_event() function is completely implemented in Java. This function will be called from the native function listen() whenever the X11 code detects a global key press.
Next we compile this Java code with:
javac KeyGrabber.java.
To implement the listen() function in C/C++ we must now use JNI. We start by
javah -jni KeyGrabber
which will generate a header file called KeyGrabber.h that looks like
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class KeyGrabber */
#ifndef _Included_KeyGrabber
#define _Included_KeyGrabber
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: KeyGrabber
* Method: listen
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Since we only had one native function in the Java code we only have one corresponding function prototype in the JNI header, namely:
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);
We must implement this function according to this prototype. We can then implement the X11 code necessary to detect global key presses in KeyGrabber.c as follows.
#include <X11/Xlib.h>
#include"KeyGrabber.h"
#include<stdio.h>
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *env, jobject obj){
printf("starting to listen to key F3 (native code)\n");
jclass cls = (*env)->FindClass(env, "KeyGrabber");
if(cls == NULL){
printf("cannot find class KeyGrabber\n") ;
exit(-1);
}
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" );
if(mid ==0){
printf("cannot find method fun\n");
exit(-1);
}
Window root;
XEvent ev;
Display * dpy = XOpenDisplay(0);
if(!dpy) return 1;
root = DefaultRootWindow(dpy);
char * key_string = "F3";
KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);
for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}
printf("leaving c code\n");
return;
}
This code listens for any key press of F3 with any modifier key, and when it detects such a key, calls the Java function "fire_key_event".
Here is a detailed break down of the sections of the code:
jclass cls = (*env)->FindClass(env, "KeyGrabber");
The jclass class is defined in jni.h which is included when you include "KeyGrabber.h". The env pointer that is passed to every native function is essentially pointing to the JVM. You can manipulate anything that the JVM can manipulate.
Our immediate goal is to be able to call the fire_key_event method in KeyGrabber class whenever the X11 code detects a global key press. The first order of business is to get a handle on the KeyGrabber class, which is what this line does.
The next order of business is to get a handle on the fire_key_event method in the KeyGrabber class. That is what
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" ); does.
The first three parameters passed to GetStaticMethodID are clear: pointer to the environment (JVM), the class with the method, the name of the method. The last parameter is the signature of the function (this of course is required because of method overloading.) To get the signature of the desired method use the following command:
javap -s KeyGrabber
which for this example gives:
Compiled from "KeyGrabber.java"
class KeyGrabber extends java.lang.Object{
KeyGrabber();
Signature: ()V
public static void fire_key_event();
Signature: ()V
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
static {};
Signature: ()V
}
and then you can copy and paste the signature straight from terminal ;).
The next bit of code
Window root;
XEvent ev;
Display * dpy = XOpenDisplay(0);
if(!dpy) return 1;
root = DefaultRootWindow(dpy);
char * key_string = "F3";
KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);
is the X11 needed to capture global key presses (in this case only key presses of F3 with any modifier.) I won't go into too much detail, but essentially the XGrabKey function call tells X11 that your code is interested in capturing key press of the F3 key with any modifier on display 0 in the root window (meaning globally.) So the following code
for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}
is an infinite loop that listens to all the registered events (in this case only F3 key presses.) If the event type is a KeyPress and the keycode is F3 then the C code will use the env pointer to call the static void method fire_key_event in the KeyGrabber class.
And its that straightforward (hopefully.)
The last order of business is to compile everything and get it running.
To compile the C code, it is a bit tricky. Provided you have a valid Java dev installation you can compile it with:
gcc --shared -o libKeyGrabber.so -I<path to java>/include -I<path to java>/include/linux -lX11 KeyGrabber.c <path to java>/jre/lib/i386/server/libjvm.so
Then to run the Java you must make sure that Java can link to the libKeyGrabber.so library that you have just compiled, so use the following command
java -Djava.library.path=. KeyGrabber
and everything should work! (eh hopefully.)
I've probably missed a lot of things so I hope you won't hesitate to post any questions and comments!