Comment obtenir une fenêtre / un processus de premier plan en Java (en utilisant JNA) sur MacOS?
Actuellement, je travaille pour obtenir la fenêtre / processus de premier plan (en haut) dans MS Windows. Je dois faire quelque chose de similaire dans macOS en utilisant JNA.
Quel est le code équivalent dans macOS?
byte[] windowText = new byte[512];
PointerType hwnd = User32.INSTANCE.GetForegroundWindow();
User32.INSTANCE.GetWindowTextA(hwnd, windowText, 512);
System.out.println(Native.toString(windowText));
Réponses
Il y a en fait deux questions ici, la fenêtre de premier plan et le processus de premier plan. Je vais essayer de répondre aux deux.
Pour le processus de premier plan , un moyen simple d'utiliser JNA consiste à mapper l' API Application Services . Notez que ces fonctions ont été introduites dans la version 10.9 et sont désormais obsolètes, mais fonctionnent toujours à partir de la version 10.15. La version la plus récente est dans le AppKitLibrary
, voir ci-dessous.
Créez cette classe en mappant les deux fonctions dont vous aurez besoin:
public interface ApplicationServices extends Library {
ApplicationServices INSTANCE = Native.load("ApplicationServices", ApplicationServices.class);
int GetFrontProcess(LongByReference processSerialNumber);
int GetProcessPID(LongByReference processSerialNumber, IntByReference pid);
}
Le processus "premier plan" peut être obtenu avec GetFrontProcess(). Cela renvoie quelque chose appelé a ProcessSerialNumber
, une valeur 64 bits unique utilisée dans l'API des services d'application. Pour le traduire pour votre utilisation en espace utilisateur, vous avez probablement besoin de l'ID de processus et GetProcessPID()faites cette traduction pour vous.
LongByReference psn = new LongByReference();
IntByReference pid = new IntByReference();
ApplicationServices.INSTANCE.GetFrontProcess(psn);
ApplicationServices.INSTANCE.GetProcessPID(psn, pid);
System.out.println("Front process pid: " + pid.getValue());
Bien que ce qui précède fonctionne, il est obsolète. Une nouvelle application doit utiliser AppKitLibrary
:
public interface AppKitLibrary extends Library {
AppKitLibrary INSTANCE = Native.load("AppKitLibrary", AppKitLibrary.class);
}
Il existe plusieurs autres questions StackOverflow concernant l'application la plus élevée utilisant cette bibliothèque, telle que celle-ci . Mapper toutes les importations et tous les objets nécessaires est beaucoup plus de travail que je n'ai le temps de le faire dans une réponse ici, mais vous pourriez le trouver utile. Il est probablement plus facile de comprendre comment utiliser le framework Rococoa (qui utilise JNA sous le capot mais a déjà mappé tout AppKit via JNAerator) pour accéder à cette API. Certains javadocs sont ici .
Il y a aussi des solutions à l' aide AppleScript
que vous pouvez exécuter à partir de Java via la ligne de commande à l' aide Runtime.exec()
et la capture de sortie.
En ce qui concerne la fenêtre de premier plan à l'écran, c'est un peu plus compliqué. Dans ma réponse à votre question précédente sur l'itération de toutes les fenêtres sur macOS, j'ai répondu comment obtenir une liste de toutes les fenêtres utilisant CoreGraphics
via JNA, y compris un CFDictionary
contenant plus d'informations.
L'une de ces clés de dictionnaire est de kCGWindowLayerrenvoyer un CFNumber
représentant le numéro de couche de la fenêtre. La documentation indique qu'il s'agit de 32 bits, ce qui intValue()
est approprié. Le nombre est «l'ordre de dessin», donc un nombre plus élevé écrasera un nombre inférieur. Ainsi, vous pouvez parcourir toutes les fenêtres récupérées et trouver le nombre maximum. Ce sera le calque "premier plan".
Il y a quelques mises en garde:
- Il n'y a en fait que 20 couches disponibles. Beaucoup de choses partagent une couche.
- La couche 1000 est l'économiseur d'écran. Vous pouvez ignorer les couches 1000 et supérieures.
- La couche 24 est le Dock, généralement sur le dessus, avec la couche 25 (les icônes sur le dock) à un niveau supérieur.
- La couche 0 semble être le reste du bureau.
- La fenêtre "en haut" dépend de l'endroit où vous regardez sur l'écran. Au-dessus du dock, le dock sera au premier plan (ou l'icône de l'application). Sur le reste de l'écran, vous devez vérifier le pixel que vous évaluez par rapport au rectangle d'écran obtenu à partir de la fenêtre CoreGraphics. (Utilisez la
kCGWindowBounds
clé qui renvoie aCGRect
(une structure avec 4 doubles, X, Y, largeur, hauteur).
Vous devrez filtrer sur les fenêtres à l'écran. Si vous avez déjà récupéré la liste, vous pouvez utiliser la kCGWindowIsOnscreenclé pour déterminer si la fenêtre est visible. Il renvoie un CFBoolean
. Étant donné que cette clé est facultative, vous devrez tester null
. Cependant, si vous partez de rien, il serait préférable d'utiliser la kCGWindowListOptionOnScreenOnly
constante d'option de fenêtre lors de l'appel initial CGWindowListCopyWindowInfo().
En plus d'itérer toutes les fenêtres, la CGWindowListCopyWindowInfo()fonction prend un CGWindowIDparamètre relativeToWindow
et vous pouvez ajouter (avec ou kCGWindowListOptionOnScreenAboveWindow
au niveau du bit) aux options.
Enfin, vous pouvez trouver que la limitation aux fenêtres associées à la session en cours peut être utile, et vous devez mapper en CGWindowListCreate()utilisant une syntaxe similaire à la CopyInfo()
variante. Il renvoie un tableau de numéros de fenêtre auquel vous pouvez limiter votre recherche dans le dictionnaire ou passer ce tableau en argument CGWindowListCreateDescriptionFromArray().
Comme mentionné dans ma réponse précédente, vous "possédez" chaque objet que vous créez en utilisant Create
ou Copy
fonctions, et êtes responsable de les libérer lorsque vous en avez terminé avec eux, pour éviter les fuites de mémoire.
AppleScriptEngine appleEngine = new apple.applescript.AppleScriptEngine();
ArrayList<String> processNames = null;
try {
String processName = null;
processNames = (ArrayList<String>) appleEngine
.eval("tell application \"System Events\" to get name of application processes whose frontmost is true and visible is true");
if (processNames.size() > 0) {
processName = processNames.get(0);// the front most process name
}
return processName;
} catch (ScriptException e) {
log.debug("no app running");
}