Come ottenere una finestra/processo in primo piano in Java (usando JNA) su MacOS?

Aug 18 2020

Attualmente, sto lavorando per ottenere la finestra/processo in primo piano (in alto) in MS Windows. Devo fare qualcosa di simile in macOS usando JNA.

Qual è il codice equivalente in macOS?

  byte[] windowText = new byte[512];
  PointerType hwnd = User32.INSTANCE.GetForegroundWindow();  
  User32.INSTANCE.GetWindowTextA(hwnd, windowText, 512);
  System.out.println(Native.toString(windowText));  

Risposte

2 DanielWiddis Aug 18 2020 at 22:53

In realtà ci sono due domande qui, finestra in primo piano e processo in primo piano. Cercherò di rispondere a entrambi.


Per il processo in primo piano, un modo semplice per utilizzare JNA consiste nel mappare l' API dei servizi applicativi . Si noti che queste funzioni sono state introdotte in 10.9 e ora sono deprecate, ma funzionano ancora a partire da 10.15. La versione più recente è in AppKitLibrary, vedi sotto.

Crea questa classe, mappando le due funzioni di cui avrai bisogno:

public interface ApplicationServices extends Library {
    ApplicationServices INSTANCE = Native.load("ApplicationServices", ApplicationServices.class);

    int GetFrontProcess(LongByReference processSerialNumber);
    int GetProcessPID(LongByReference processSerialNumber, IntByReference pid);
}

Il processo "in primo piano" può essere ottenuto con GetFrontProcess(). Ciò restituisce qualcosa chiamato a ProcessSerialNumber, un valore univoco a 64 bit utilizzato in tutta l'API dei servizi applicativi. Per tradurlo per il tuo uso nello spazio utente, probabilmente vorrai l'ID processo e GetProcessPID()fa quella traduzione per te.

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());

Mentre quanto sopra funziona, è deprecato. Una nuova applicazione dovrebbe utilizzare AppKitLibrary:

public interface AppKitLibrary extends Library {
    AppKitLibrary INSTANCE = Native.load("AppKitLibrary", AppKitLibrary.class);
}

Esistono molte altre domande su StackOverflow riguardanti l'applicazione più in alto che utilizza questa libreria, come questa . Mappare tutte le importazioni e gli oggetti necessari è molto più lavoro di quanto io abbia il tempo di fare in una risposta qui, ma potresti trovarlo utile. Probabilmente è più facile capire come utilizzare il framework Rococoa (che utilizza JNA sotto il cofano ma ha già mappato tutto AppKit tramite JNAerator) per accedere a questa API. Alcuni javadoc sono qui .

Esistono anche soluzioni che possono essere eseguite da Java tramite AppleScriptla riga di comando utilizzando Runtime.exec()e catturando l'output.


Per quanto riguarda la finestra in primo piano sullo schermo, è un po' più complicato. Nella mia risposta alla tua precedente domanda sull'iterazione di tutte le finestre su macOS, ho risposto a come ottenere un elenco di tutte le finestre che utilizzano CoreGraphicstramite JNA, incluso un CFDictionarycontenente ulteriori informazioni.

Una di quelle chiavi del dizionario è kCGWindowLayerche restituirà un che CFNumberrappresenta il numero del livello della finestra. I documenti affermano che questo è a 32 bit, quindi intValue()è appropriato. Il numero è l'"ordine di disegno", quindi un numero più alto sovrascriverà un numero più basso. Quindi puoi scorrere tutte le finestre recuperate e trovare il numero massimo. Questo sarà il livello "in primo piano".

Ci sono alcuni avvertimenti:

  • In realtà ci sono solo 20 livelli disponibili. Molte cose condividono uno strato.
  • Layer 1000 è lo screensaver. Puoi ignorare i livelli 1000 e successivi.
  • Il livello 24 è il Dock, solitamente in alto, con il livello 25 (le icone sul dock) a un livello superiore.
  • Il livello 0 sembra essere il resto del desktop.
  • Quale finestra è "in alto" dipende da dove guardi sullo schermo. Sopra il dock, il dock sarà in primo piano (o l'icona dell'applicazione). Sul resto dello schermo, devi controllare il pixel che stai valutando rispetto al rettangolo dello schermo ottenuto dalla finestra CoreGraphics. (Usa la kCGWindowBoundschiave che restituisce a CGRect(una struttura con 4 doppi, X, Y, larghezza, altezza).

Dovrai filtrare le finestre sullo schermo. Se hai già recuperato l'elenco, puoi utilizzare la kCGWindowIsOnscreenchiave per determinare se la finestra è visibile. Restituisce un CFBoolean. Poiché quella chiave è facoltativa, dovrai testare null. Tuttavia, se stai partendo da zero, sarebbe meglio usare la kCGWindowListOptionOnScreenOnly costante di opzione della finestra quando inizialmente chiami CGWindowListCopyWindowInfo().

Oltre a iterare tutte le finestre, la CGWindowListCopyWindowInfo()funzione prende un CGWindowIDparametro relativeToWindowe puoi aggiungere (con bit a bit o) kCGWindowListOptionOnScreenAboveWindowalle opzioni.

Infine, potresti scoprire che la limitazione alle finestre associate alla sessione corrente può essere utile e dovresti mappare CGWindowListCreate()utilizzando una sintassi simile alla CopyInfo()variante. Restituisce un array di numeri di finestra a cui puoi limitare la ricerca nel dizionario o passare quell'array come argomento a CGWindowListCreateDescriptionFromArray().

Come accennato nella mia risposta precedente, "possiedi" ogni oggetto che crei utilizzando Createo Copyfunzioni e sei responsabile del loro rilascio quando hai finito con loro, per evitare perdite di memoria.

BinLiu Oct 18 2020 at 18:31
   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");
    }