19.06.02 Windows KeyEvents abfangen

In diesem Teil der Kapitel-Serie JNI für Windows bekommen Sie eine Möglichkeit gezeigt, wie Sie mit einem C Programm alle Tastendrücke auf einem Windows-Betriebssystem abfangen, und an ein Java-Programm via JNI weiterleiten können. Die hier vorgestellte Technik darf selbstverständlich nicht zur Entwicklung bösartiger und/oder illegaler Software wie bspw. KeyLogger zur Ausspähung von Benutzerdaten und Passwörtern verwendet werden.

Der Java Code

Zur Demonstration der Funktionsweise legen Sie sich zuerst eine einfache Java-Klasse an: JGlobalKeyListener. Diese läd wie gewohnt die spätere DLL keylistener.dll, ruft bei der Initialisierung die native Funktion listen() auf, und bekommt über die Methode keyPressed(int keycode, char keyChar, boolean shiftDown, boolean strgDown, boolean altDown) von der DLL mitgeteilt, falls eine Taste gedrückt wurde. Der keyCode ist der MSDN-KeyCode, keyChar ist das Zeichen, das von Windows zu diesem keyCode assoziiert wird (ohne Rücksicht auf Groß- und Kleinschreibung), shiftDown, strgDown und altDown geben jeweils an, ob eine der drei Tasten gedrückt wurde.

package de.jbb.jni;

public class JGlobalKeyListener {

  static {  
    System.loadLibrary("keylistener");  
  } 

  public JGlobalKeyListener() {

    listen();
  }

  private native void listen();

  private void keyPressed(int keyCode, char keyChar, boolean shiftDown, boolean strgDown, boolean altDown) {

    System.out.println("KeyPressed: " + keyCode + "/" + keyChar + (shiftDown ? "/shift" : "") + (strgDown ? "/strg" : "") + (altDown ? "/alt" : ""));
  }

  public static void main(String[] args) {
    new JGlobalKeyListener();
  }
}

Der C-Code

Beim Zugriff auf Windows-Funktionen wird die Header-Datei windows.h verwendet. Diese müssen Sie als ersten Schritt in Ihre C-Library mitsamt der Header-Datei des Java-Programms und dem JNI Header einbinden. Zusätzlich wird gleich der Kopf der listen-Methode eingesetzt.

#include <jni.h>
#include <windows.h>
#include "de_jbb_jni_JGlobalKeyListener.h"

JNIEXPORT void JNICALL Java_de_jbb_jni_JGlobalKeyListener_listen(JNIEnv *env, jobject obj)   {

}

In der listen-Methode benötigen Sie folgende Variablen:

  • int keyCode – Variable für den aktuellen MSDN-KeyCode
  • boolean shift, alt, strg – Diese Variablen speichern den Status (gedrückt, nicht gedrückt) der jeweiligen Taste
  • jclass icls – Hier wird eine Referenz auf die aufrufende Klasse gespeichert, damit die keyPressed Methode ermittelt werden kann
  • jmethodID jmid – Repräsentiert die keyPressed Methode
int keyCode;
boolean shift;
boolean alt;
boolean strg;
jclass icls = (*env)->GetObjectClass(env, obj);
jmethodID jmid = (*env)->GetMethodID(env, icls, "keyPressed", "(ICZZZ)V");

Nach der Deklarierung/Initialisierung der benötigten Variablen wird eine Endlosschleife gestartet, die ständig die Betätigung einer Taste überprüft und anschließend – um die Rechnerauslastung nicht auf 100% zu treiben – kurz schläft/pausiert.

while (TRUE) {
  Sleep(5);
}

In der Schleife wird zuerst die SHIFT, ALT und STRG-Taste überprüft. Falls die Windows-Funktion GetAsyncKeyState einen Wert ungleich 0 zurückliefert, ist die Taste aktuelle gedrückt, oder wurde seit dem letzten Aufruf von GetAsyncKeyState gedrückt. Genau genommen ist die Taste momentan gedrückt, wenn das most significant bit gesetzt ist (Wikipedia: MSB), bzw. sie wurde seit dem letzten Aufruf gedrückt, wenn das least significat bit gesetzt ist (Wikipedia: LSB). Es muss also überprüft werden, ob der Rückgabewert ungleich 0 ist.

shift = GetAsyncKeyState(VK_SHIFT) != 0;
alt = GetAsyncKeyState(VK_MENU) != 0;
strg = GetAsyncKeyState(VK_CONTROL) != 0;

Anschließend wird eine neue Schleife gestartet, welche die MSDN-KeyCodes von 1 bis 256 auf identische Weise überprüft. Diese Überprüfung bezieht sich dieses Mal jedoch nur auf das LSB, da es sinnvoller ist, nur einmal pro Tastendruck die betätigte Taste abzufragen.

for (keyCode = 256; keyCode > 0; keyCode--) {
  if (GetAsyncKeyState(keyCode) & 1) {
  }
}

Falls die Taste nun seit dem letzten Aufruf gedrückt wurde, kann die keyPressed Methode des JGlobalKeyListeners aufgerufen werden. Den keyCode und die Stati der Tasten SHIFT, ALT und STRG haben Sie bereits ermittelt. Es fehlt noch die Umwandlung des keyCodes in das keyChar. Diese erhalten Sie durch den Funktionsaufruf MapVirtualKey(uCode, uMapType), wobei uCode Ihr keyCode ist, und uMapType ein Wert, der spezifiziert, in was der gegebene Input umgewandelt werden soll. Für unseren Fall bietet sich MAPVK_VK_TO_CHAR an, was einer 2 entspricht.

(*env)->CallVoidMethod(env, obj, jmid, 
  keyCode,
  (char)MapVirtualKey(keyCode, 2),
  shift,
  strg,
  alt
);

Mehr ist nicht nötig. Die gesamte C-Bibliothek sieht wie folgt aus:

#include <jni.h>
#include <windows.h>
#include "de_jbb_jni_JGlobalKeyLogger.h"

JNIEXPORT void JNICALL Java_de_jbb_jni_JGlobalKeyListener_listen(JNIEnv *env, jobject obj)   {

  int keyCode;
  boolean shift;
  boolean alt;
  boolean strg;
  jclass icls = (*env)->GetObjectClass(env, obj);
  jmethodID jmid = (*env)->GetMethodID(env, icls, "keyPressed", "(ICZZZ)V");
  while (TRUE) {
    shift = GetAsyncKeyState(VK_SHIFT) != 0;
    alt = GetAsyncKeyState(VK_MENU) != 0;
    strg = GetAsyncKeyState(VK_CONTROL) != 0;
    for (keyCode = 256; keyCode > 0; keyCode--) {
      if (GetAsyncKeyState(keyCode) & 1) {
        (*env)->CallVoidMethod(env, obj, jmid, 
          keyCode,
          (char)MapVirtualKey(keyCode, 2),
          shift,
          strg,
          alt); 	
      }
    }
    Sleep(5);
  } 
  return;
}

Ausgabe

Je nach gedrückten Tasten könnte eine Ausgabe so aussehen:

KeyPressed: 161/ /shift
KeyPressed: 161/ /shift
KeyPressed: 65/A/shift
KeyPressed: 68/D/shift
KeyPressed: 76/L/shift
KeyPressed: 162/ /strg
KeyPressed: 164/ /strg/alt
KeyPressed: 75/K/strg/alt
KeyPressed: 56/8/strg/alt
KeyPressed: 187/+
KeyPressed: 191/#
KeyPressed: 222/?

Ausblick

Das Ganze lässt sich natürlich noch weiter perfektionieren und ausbauen – insbesondere dann, wenn man für die jeweiligen Tastaturlayouts die keyCodes korrekt interpretiert. Einen ersten Ansatz hierzu bietet die OpenSource Bibliothek Java Global Key Listener.

2 Replies to “19.06.02 Windows KeyEvents abfangen”

  1. Sebastian

    Hallo Stefan,

    ich haette mal eine generelle Frage zu JNI. Ich bin Auszubildender FIAE und wir schreiben Toolboxen fuer Drucker in der Sprache C++ und WinApi32 Funktionen. Um das ganze mal kurz zu erklaeren, musst du mit CreateFile ein handle zum Drucker erstellen um mit diesem zu kommunizieren. Dann kannst du mit SetupDiGetEnumDevices einen per USB angeschlossenen Drucker auswaehlen und kommunizieren. Mit ReadFile liest du einen String vom Drucker aus, veraenderst und wertest diesen aus und schickst mit WriteFile den neuen String an den Drucker.

    Ich moechte nun meinen Horizont etwas erweitern und das ganze mal in JAVA versuchen. Ich weiss nur nicht ganz wo ich da anfangen soll. Wenn ich das richtig verstanden habe, kann ich mit JNI C/C++ Quellcode einbinden und verwenden? Oder hat JAVA da ganz eigene Wege zur kommunikation? Ich kann ja zum Beispiel unter Linux keine Windows.h einbinden …

    Ueber ein Antwort wuerde ich mich sehr freuen!

    Gruesse,
    Sebastian

  2. Stefan Kiesel

    Hallo Sebastian,

    erst einmal viel Erfolg für den weiteren Verlauf Ihrer Ausbildung.

    Java ist generell plattformunabhängig. Deshalb ist es schwierig systemspezifische oder hardwarenahe Funktionen und Komponenten anzusteuern. Da fahren Sie mit C++ schon den deutlich sinnvolleren Weg.

    Es gibt zwar sicher für einige spezielle Dinge standardisierte Lösungswege, die auch mehr oder weniger plattformunabhängig sind, aber generell halte ich es für sinniger so etwas in C++/C zu machen.

    Generell könnten Sie diese daraus resultierenden Programmteile auch via JNI in Java einbinden, die nativen Bibliotheken müssten trotzdem noch für jedes Betriebssystem separat geschrieben bzw. zumindest kompiliert werden. Zu Lernzwecken für Ihre Ausbildung sicherlich erwägenswert, aber für die Praxis würde ich eher davon abraten.

    Ich hoffe ich konnte Ihnen weiter helfen. Ansonsten können Sie gerne noch einmal nachfragen.

    Grüße
    Stefan

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.