Регистрация глобальных нажатий клавиш с использованием JNA
Здравствуйте, в этой статье я расскажу вам как регистрировать глобальные нажатия клавиш из Java под Windows, Linux, BSD и Mac OSX с использованием отличной библиотеки JNA.
Для чего нужен JNA
Java с десктопом дружит сложно, для некоторых вещей нужно писать мосты для взаимодействия с операционной системой. Одной из таких функциональностей являются глобальные хоткеи, весьма популярные в аудио плеерах, когда даже в скрытом состоянии программой можно управлять с помощью определенных сочетаний клавиш или медиа-кнопок. На помощь приходит JNA — надстройка над jni и libffi для вызова нативных библиотек, она поддерживает почти все популярные платформы, разрабатывается уже долгое время и весьма стабильна.
Для джавы уже есть несколько достаточно стабильных библиотек для всех платформ: JIntelliType для Windows, которая даже поддерживает медиа-кнопки, JXGrabKey для систем Linux, и ossuport-connector для Mac OSX. Однако, все они используют jni, имеют разный интерфейс, и с библиотеками на jni не всегда удобно работать, потому что нужно прописывать пути к нативным библиотекам, разбираться с разрядностью системы и пр. Плюс это будет интересным упражнением по использованию JNA, потому что эту задачу можно сделать полностью на java достаточно малыми усилиями и получить легко поддерживаемый кроссплатформенный код.
Windows
Проще всего с глобальными хоткеями работать в Windows:
public class User32 {
static {
Native.register(NativeLibrary.getInstance("user32", W32APIOptions.DEFAULT_OPTIONS));
}
public static final int MOD_ALT = 0x0001;
public static final int MOD_CONTROL = 0x0002;
public static final int MOD_SHIFT = 0x0004;
public static final int MOD_WIN = 0x0008;
public static final int WM_HOTKEY = 0x0312;
public static native boolean RegisterHotKey(Pointer hWnd, int id, int fsModifiers, int vk);
public static native boolean UnregisterHotKey(Pointer hWnd, int id);
public static native boolean PeekMessage(MSG lpMsg, Pointer hWnd, int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);
public static class MSG extends Structure {
public Pointer hWnd;
public int message;
public int wParam;
public int lParam;
public int time;
public int x;
public int y;
}
}
Здесь мы используем так называемый direct mapping. Помимо того что он быстрее, с ним проще работать, так как можно сделать static import и использовать методы как родные. Нам нужны три метода из User32:
-
RegisterHotKey — собственно регистрирует хоткей. Первым параметром можно ставить null, так как окна у нас нет. Второй параметр — уникальный номер нашего хоткея, генерируется нами, он после будет использован для идентификация хоткея и для его удаления через UnregisterHotKey. В fsModifiers пишем какие модификаторы нам нужны (ctrl, shift, alt или win). В vk пишем виртуальный код. Интересно, что виртуальные коды используемые в KeyEvent в AWT почти всегда совпадают с виртуальными кодами в Windows:
// регистрируем сочетание WIN+F
RegisterHotKey(null, 1, 0x8, KeyEvent.VK_F);
-
PeekMessage — проверяет входящие сообщения и забивает их в структуру MSG. Здесь используется неблокирующий вызов PeekMessage вместо блокирующего GetMessage из-за того что все методы должны вызываться из одного потока и нам необходимо иметь возможность этим потоком управлять:
MSG msg = new MSG();
while (listen) {
while (PeekMessage(msg, null, 0, 0, PM_REMOVE)) {
if (msg.message == WM_HOTKEY) {
System.out.println("Yattaaaa. Hotkey with id " + msg.wParam);
}
}
Thread.sleep(300);
}
Здесь мы регистрируем сочетание WIN+F и проверяем сообщения каждые 300 мс.
X11
К сожалению, для X11 direct mapping использовать не получится, так как это почему-то вызывает ошибки при работе с FreeBSD. Маппинг выглядит немного сложнее:
public interface X11 extends Library {
public static X11 Lib = (X11) Native.loadLibrary("X11", X11.class);
public static final int GrabModeAsync = 1;
public static final int KeyPress = 2;
public static final int ShiftMask = (1);
public static final int LockMask = (1 << 1);
public static final int ControlMask = (1 << 2);
public static final int Mod1Mask = (1 << 3);
public static final int Mod2Mask = (1 << 4);
public static final int Mod3Mask = (1 << 5);
public static final int Mod4Mask = (1 << 6);
public static final int Mod5Mask = (1 << 7);
public Pointer XOpenDisplay(String name);
public NativeLong XDefaultRootWindow(Pointer display);
public byte XKeysymToKeycode(Pointer display, long keysym);
public int XGrabKey(Pointer display, int code, int modifiers, NativeLong root, int ownerEvents, int pointerMode, int keyBoardMode);
public int XUngrabKey(Pointer display, int code, int modifiers, NativeLong root);
public int XNextEvent(Pointer display, XEvent event);
public int XPending(Pointer display);
public int XCloseDisplay(Pointer display);
public static class XEvent extends Union {
public int type;
public XKeyEvent xkey;
public NativeLong[] pad = new NativeLong[24];
}
public static class XKeyEvent extends Structure {
public int type; // of event
public NativeLong serial; // # of last request processed by server
public int send_event; // true if this came from a SendEvent request
public Pointer display; // public Display the event was read from
public NativeLong window; // "event" window it is reported relative to
public NativeLong root; // root window that the event occurred on
public NativeLong subwindow; // child window
public NativeLong time; // milliseconds
public int x, y; // pointer x, y coordinates in event window
public int x_root, y_root; // coordinates relative to root
public int state; // key or button mask
public int keycode; // detail
public int same_screen; // same screen flag
}
}
- XOpenDisplay, XCloseDisplay, XDefaultRootWindow — получают и закрывают дефолтный дисплей и коренное окно.
- XKeysymToKeycode — конвертирует символ (описания символов берутся в keysymdef.h) в клавитурный код, он потребуется позже:
// для букв и цифр номер символа также соответствует значению в KeyEvent
byte code = XKeysymToKeycode(display, KeyEvent.VK_F);
- XGrabKey, XUngrabKey — регистрируют и удаляют хоткей. В code здесь пишется значение полученное ранее с XKeysymToKeycode, в modifiers пишутся модификаторы, в root значение из XDefaultRootWindow. Остальные значения забиваются единицами. Интересно, что в X11 нажатые ScrollLock, NumLock, CapsLock и еще какой-то Lock также считаются модификаторами, поэтому вместо одного хоткея приходится регистрировать 16, для каждой возможной комбинации:
// этот хак найден в плагине global hotkey в плеере deadbeef
for (int flags = 0; flags < 16; i++) {
int ret = modifiers;
if ((flags & 1) != 0)
ret |= LockMask;
if ((flags & 2) != 0)
ret |= Mod2Mask;
if ((flags & 4) != 0)
ret |= Mod3Mask;
if ((flags & 8) != 0)
ret |= Mod5Mask;
XGrabKey(display, code, ret, root, 1, GrabModeAsync, GrabModeAsync);
// XUngrabKey(display, code, ret, root);
}
Также, в отличие от Windows, где удалять хоткей при выходе программы необязательно, если не вызвать XUngrabKey, то иксы будут держать его до самого перезапуска.
- XPending и XNextEvent — проверяют наличие и достают следующее событие:
while (listening) {
while (Lib.XPending(display) > 0) {
Lib.XNextEvent(display, event);
if (event.type == KeyPress) {
// считываем наше событие из union XEvent
// JNA не знает какое именно поле заполнять в union,
// поэтому ему нужно сказать какое из полей считать.
XKeyEvent xkey = (XKeyEvent) event.readField("xkey");
// очищаем мусорные модификаторы
int state = xkey.state & (ShiftMask | ControlMask | Mod1Mask | Mod4Mask);
System.out.println("Yattaaaa, hotkey with code: " + xkey.keycode + " and modifiers: " + state);
}
}
Thread.sleep(300);
}
Mac OSX
Основу кода для Mac OSX составили наработки Torsten Uhlmann, автора ossupport-connector:
public interface Carbon extends Library {
public static Carbon Lib = (Carbon) Native.loadLibrary("Carbon", Carbon.class);
public static final int cmdKey = 0x0100;
public static final int shiftKey = 0x0200;
public static final int optionKey = 0x0800;
public static final int controlKey = 0x1000;
// OS_TYPE объединяет символы строки в int
private static final int kEventClassKeyboard = OS_TYPE("keyb");
private static final int typeEventHotKeyID = OS_TYPE("hkid");
private static final int kEventParamDirectObject = OS_TYPE("----");
public Pointer GetEventDispatcherTarget();
public int InstallEventHandler(Pointer inTarget, EventHandlerProcPtr inHandler, int inNumTypes, EventTypeSpec[] inList, Pointer inUserData, PointerByReference outRef);
public int RegisterEventHotKey(int inHotKeyCode, int inHotKeyModifiers, EventHotKeyID.ByValue inHotKeyID, Pointer inTarget, int inOptions, PointerByReference outRef);
public int GetEventParameter(Pointer inEvent, int inName, int inDesiredType, Pointer outActualType, int inBufferSize, IntBuffer outActualSize, EventHotKeyID outData);
public int RemoveEventHandler(Pointer inHandlerRef);
public int UnregisterEventHotKey(Pointer inHotKey);
public class EventTypeSpec extends Structure {
public int eventClass;
public int eventKind;
}
public static class EventHotKeyID extends Structure {
public int signature;
public int id;
public static class ByValue extends EventHotKeyID implements Structure.ByValue {
}
}
public static interface EventHandlerProcPtr extends Callback {
public int callback(Pointer inHandlerCallRef, Pointer inEvent, Pointer inUserData);
}
}
- GetEventDispatcherTarget — получает ссылку на системный обработчик событий
- InstallEventHandler — добавляет к этому обработчику событий наш обработчик. В отличие от остальных платформ, в Carbon события приходят асинхронно через callback:
eventHandlerReference = new PointerByReference();
// собственно, сам обработчик
keyListener = new EventHandler();
//магия JNA для создания массива из одной структуры
EventTypeSpec[] eventTypes = (EventTypeSpec[]) (new EventTypeSpec().toArray(1));
eventTypes[0].eventClass = kEventClassKeyboard;
eventTypes[0].eventKind = kEventHotKeyPressed;
int status = Lib.InstallEventHandler(Lib.GetEventDispatcherTarget(), keyListener, 1, eventTypes, null, eventHandlerReference);
Здесь мы указываем что будем обрабатывать сообщения клавиатуры, а именно нажатия горячих клавиш.
В eventHandlerReference приходит обратная ссылка на обработчик, которая нужна в RemoveEventHandler.
Обработчик событий выглядит следующим образом:
private class EventHandler implements Carbon.EventHandlerProcPtr {
public int callback(Pointer inHandlerCallRef, Pointer inEvent, Pointer inUserData) {
EventHotKeyID eventHotKeyID = new EventHotKeyID();
// получаем параметры события
int ret = Lib.GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, null, eventHotKeyID.size(), null, eventHotKeyID);
if (ret != 0) {
logger.warning("Could not get event parameters. Error code: " + ret);
} else {
// Получаем и обрабатываем идентификатор события здесь
int eventId = eventHotKeyID.id;
logger.info("Received event id: " + eventId);
}
return 0;
}
}
- RegisterEventHotKey — регистрирует хоткей. На вход идет код клавишы. Таблица кодов находится здесь. Далее список модификаторов. Потом структура EventHotKeyID.ByValue, в которую забивается наш идентификатор и четырехбуквенная подпись. ByValue используется потому что по-умолчанию структуры передаются по ссылке, а нам нужно по значению. Возвращается ссылка на этот хоткей, которая используется в UnregisterEventHotKey:
EventHotKeyID.ByValue hotKeyReference = new EventHotKeyID.ByValue();
hotKeyReference.id = 1;
hotKeyReference.signature = OS_TYPE("hk01");
PointerByReference gMyHotKeyRef = new PointerByReference();
int status = Lib.RegisterEventHotKey(code, modifiers, hotKeyReference, Lib.GetEventDispatcherTarget(), 0, gMyHotKeyRef);
Заключение
В целом все достаточно просто, хотя и были некоторые проблемы, такие как падения при использовании direct mapping на FreeBSD, отказ JNA мапить boolean в XGrabKey на int, странные ошибки при передаче структуры по ссылке, а не по значению в Carbon, ошибки, генерируемые X11 если хоткей уже занят, которые просто вырубали программу, сложность нахождения какой-либо документации по Carbon.
Весь этот код собран в библиотеку jkeymaster под лицензией LGPL 3. Интерфейс основан на KeyStroke из Swing, для Windows и X11 можно регистрировать медиа-кнопки — Play/Pause, Stop, Next Track, Previous Track.
Замечания и патчи приветствуются.
p.s. Пост написан tulskiy, который благодаря mgarin теперь есть на хабре. Так что все плюсы ему.