如何在MacOS上使用Java(使用JNA)获取前景窗口/进程?

huangapple go评论187阅读模式
英文:

How to get foreground window/process in Java (Using JNA) on MacOS?

问题

目前,我正在努力获取 MS Windows 中的前景(顶部)窗口/进程。我需要在 macOS 中使用 JNA 执行类似操作。

在 macOS 中,等效的代码是:

import com.sun.jna.Pointer;
import com.sun.jna.platform.mac.Carbon;
import com.sun.jna.ptr.BytePtr;
import com.sun.jna.ptr.IntByReference;

Carbon.INSTANCE.GetCurrentProcess();
Carbon.INSTANCE.GetFrontProcess(new IntByReference());
Carbon.INSTANCE.CopyProcessName(new IntByReference(), new BytePtr(windowText), new IntByReference(512));
System.out.println(Native.toString(windowText));
英文:

Currently, I am working to get foreground(top) window/process in MS Windows. I need to do something similar in macOS using JNA.

What is the equivalent code in macOS?

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

答案1

得分: 3

以下是翻译好的部分:


实际上有两个问题,前台窗口和前台进程。我将尝试回答两者。


关于前台进程,使用JNA的一种简单方法是映射Application Services API。请注意,这些函数是在10.9中引入的,现在已被弃用,但在10.15仍然有效。更新的版本在AppKit库中,详见下文。

创建这个类,映射你需要的两个函数:

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

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

获取“前台”进程可以使用GetFrontProcess()。它会返回一个称为ProcessSerialNumber的东西,这是在整个Application Services API中使用的唯一的64位值。为了将它翻译成用户空间使用,您可能想要进程ID,GetProcessPID()会为您执行这种转换。

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

虽然上述方法可行,但它已经被弃用。新的应用程序应该使用AppKit库:

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

关于使用此库获取顶层应用程序的其他StackOverflow问题有很多,比如这个问题。映射所有所需的导入和对象比我在这里的回答中所能做的工作更多,但您可能会发现它有用。也许更容易弄清楚如何使用Rococoa框架(它在底层使用了JNA,但已经通过JNAerator将所有AppKit都映射了)来访问此API。一些javadocs在这里

还有一些使用AppleScript的解决方案,您可以通过在Java中使用Runtime.exec()执行并捕获输出来执行它们。


至于屏幕上的前台窗口,情况会更加复杂。在我之前回答的关于在macOS上迭代所有窗口的问题中,我解释了如何使用JNA通过CoreGraphics获取所有窗口的列表,包括一个包含更多信息的CFDictionary

其中一个字典键是kCGWindowLayer,它将返回一个表示窗口层次的CFNumber。文档说明这是32位的,所以适用intValue()。这个数字是“绘图顺序”,因此较大的数字将覆盖较小的数字。因此,您可以遍历所有检索到的窗口并找到最大的数字。这将是“前台”层次。

还有一些注意事项:

  • 实际上只有20个可用的层次。许多东西共享一个层次。
  • 层次1000是屏幕保护程序。您可以忽略1000及更高的层次。
  • 层次24是Dock,通常位于顶部,而第25层(Dock上的图标)位于较高级别。
  • 层次0似乎是剩余的桌面部分。
  • 哪个窗口“在顶部”取决于您在屏幕上查看的位置。在Dock上方,Dock将位于前景(或应用程序图标)。在屏幕的其他位置,您需要检查您正在评估的像素与从CoreGraphics窗口获取的屏幕矩形。 (使用kCGWindowBounds键,它返回一个CGRect(一个具有4个double值,X、Y、宽度、高度的结构)。

您需要筛选出屏幕上的窗口。如果您已经获取了列表,您可以使用kCGWindowIsOnscreen键来确定窗口是否可见。它返回一个CFBoolean。由于该键是可选的,您需要测试是否为null。但是,如果您从零开始,最好在最初调用CGWindowListCopyWindowInfo()时使用kCGWindowListOptionOnScreenOnly Window Option Constant

除了遍历所有窗口之外,CGWindowListCopyWindowInfo()函数还接受一个CGWindowID参数relativeToWindow,您可以将kCGWindowListOptionOnScreenAboveWindow与选项进行位或操作。

最后,您可能会发现将限制为与当前会话关联的窗口可能会有用,并且您应该使用与CopyInfo()变体类似的语法映射CGWindowListCreate()。它返回一个窗口号的数组,您可以将字典搜索限制为该数组,或者将该数组作为参数传递给CGWindowListCreateDescriptionFromArray()

正如我在之前的回答中提到的,您通过CreateCopy函数“拥有”您创建的每个对象,并且在使用完毕后负责释放它们,以避免内存泄漏。

英文:

There are actually two questions here, foreground window and foreground process. I'll try to answer both.

<hr>

For the foreground process an easy way using JNA is to map Application Services API. Note that these functions were introduced in 10.9 and are now deprecated, but still work as of 10.15. The newer version is in the AppKit Library, see below.

Create this class, mapping the two functions you'll need:

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

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

The "foreground" process can be obtained with GetFrontProcess(). That returns something called a ProcessSerialNumber, a unique 64-bit value used throughout the Application Services API. To translate it for your userspace use, you probably want the Process ID, and GetProcessPID() does that translation for you.

LongByReference psn = new LongByReference();
IntByReference pid = new IntByReference();
ApplicationServices.INSTANCE.GetFrontProcess(psn);
ApplicationServices.INSTANCE.GetProcessPID(psn, pid);
System.out.println(&quot;Front process pid: &quot; + pid.getValue());

<hr>

While the above works, it is deprecated. A new application should use the AppKit Library:

public interface AppKit extends Library {
    AppKit INSTANCE = Native.load(&quot;AppKit&quot;, AppKit.class);
}

There are multiple other StackOverflow questions regarding the topmost application using this library, such as this one. Mapping all the imports and objects needed is far more work than I have time to do in an answer here, but you might find it useful. It's probably easier to figure out how to use the Rococoa framework (which uses JNA under the hood but has already mapped all of AppKit via JNAerator) to access this API. Some javadocs are here.

There are also solutions using AppleScript that you can execute from Java via command line using Runtime.exec() and capturing output.

<hr>

With regard to foreground window on the screen, it's a bit more complicated. In my answer to your earlier question on iterating all windows on macOS, I answered how to get a list of all the windows using CoreGraphics via JNA, including a CFDictionary containing more information.

One of those dictionary keys is kCGWindowLayer which will return a CFNumber representing the window layer number. The docs state this is 32-bit, so intValue() is appropriate. The number is the "drawing order" so a higher number will overwrite a lower number. So you can iterate over all the retrieved windows and find the maximum number. This will be the "foreground" layer.

There are some caveats:

  • There are actually only 20 layers available. Many things share a layer.
  • Layer 1000 is the screensaver. You can ignore layers 1000 and higher.
  • Layer 24 is the Dock, usually on top, with Layer 25 (the icons on the dock) at a higher level.
  • Layer 0 appears to be the rest of the desktop.
  • Which window is "on top" depends on where on the screen you look. Over the dock, the dock will be in the foreground (or the application icon). On the rest of the screen, you need to check the pixel you're evaluating vs. the screen rectangle obtained from the CoreGraphics window. (Use the kCGWindowBounds key which returns a CGRect (a structure with 4 doubles, X, Y, width, height).

You will need to filter to onscreen windows. If you already fetched the list you could use the kCGWindowIsOnscreen key to determine whether the window is visible. It returns a CFBoolean. Since that key is optional you will need to test for null. However, if you are starting from nothing, it would be better to use the kCGWindowListOptionOnScreenOnly Window Option Constant when you initially call CGWindowListCopyWindowInfo().

In addition to iterating all windows, the CGWindowListCopyWindowInfo() function takes a CGWindowID parameter relativeToWindow and you can add (with bitwise or) kCGWindowListOptionOnScreenAboveWindow to the options.

Finally, you might find that limiting to windows associated with the current session may be useful, and you should map CGWindowListCreate() using similar syntax to the CopyInfo() variant. It returns an array of window numbers that you could limit your dictionary search to, or pass that array as an argument to CGWindowListCreateDescriptionFromArray().

As mentioned in my previous answer, you "own" every object you create using Create or Copy functions, and are responsible for releasing them when you are done with them, to avoid memory leaks.

答案2

得分: 1

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");
}
英文:
   AppleScriptEngine appleEngine = new apple.applescript.AppleScriptEngine();
    ArrayList&lt;String&gt; processNames = null;
    try {
    	String processName = null;
    	processNames = (ArrayList&lt;String&gt;) appleEngine
    			.eval(&quot;tell application \&quot;System Events\&quot; to get name of application processes whose frontmost is true and visible is true&quot;);
    	if (processNames.size() &gt; 0) {
    		processName = processNames.get(0);// the front most process name
    	}
    	return processName;
    } catch (ScriptException e) {
    	log.debug(&quot;no app running&quot;);
    }

huangapple
  • 本文由 发表于 2020年8月18日 21:30:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/63469738.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定