在Java中找出当前处于焦点的应用程序(窗口)

26

我想知道如何编写一个Java程序,可以知道哪个Windows应用程序处于焦点状态。我可能打开了很多窗口,但我想知道正在使用的窗口(就像我现在输入这些文字时使用的Google Chrome窗口一样)。

我不需要更改窗口或应用程序中的任何内容,只需要知道它的名称。


你想用这个窗口做什么?可能只有使用JNI才能找到它。 - Stan Kurilin
如果可能的话,展示如何在所有三个主要平台上实现,即Windows、Mac和Linux,这将非常棒。 - Ali
3个回答

35

正如其他人已经指出的,没有一种可移植的方式可以在所有平台上获取这个信息。但更糟糕的是:在MS Windows上甚至没有一种一致的方式。我将提供一些代码,以解决不同平台上的问题,并指出其限制。请自行承担使用风险,由于安全原因,该代码可能会提供错误的结果或根本无法运行。如果它在您的机器上运行,这并不意味着它在其他机器上也能同样良好地运行。

代码使用JNA。在我的实验中,我遇到了不同版本的JNA和JNA平台库的问题。最好自己编译它,这样就可以获得一致的环境。

Windows

kichik所提供的答案在当时是正确的,但在某些情况下在Windows 8上无法正常工作。问题在于它无法正确处理Metro应用程序。不幸的是,目前还没有稳定的API来获取当前正在运行的Metro应用程序的名称。我已经在代码中插入了一些提示,但最好等待微软提供API。

在Windows上,您还会遇到特权应用程序和UAC对话框的问题。因此,您将不总是能够获得正确的答案。

public interface Psapi extends StdCallLibrary {
    Psapi INSTANCE = (Psapi) Native.loadLibrary("Psapi", Psapi.class);

    WinDef.DWORD GetModuleBaseNameW(Pointer hProcess, Pointer hModule, byte[] lpBaseName, int nSize);
}
    if (Platform.isWindows()) {
        final int PROCESS_VM_READ=0x0010;
        final int PROCESS_QUERY_INFORMATION=0x0400;
        final User32 user32 = User32.INSTANCE;
        final Kernel32 kernel32=Kernel32.INSTANCE;
        final Psapi psapi = Psapi.INSTANCE;
        WinDef.HWND windowHandle=user32.GetForegroundWindow();
        IntByReference pid= new IntByReference();
        user32.GetWindowThreadProcessId(windowHandle, pid);
        WinNT.HANDLE processHandle=kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, true, pid.getValue());

        byte[] filename = new byte[512];
        Psapi.INSTANCE.GetModuleBaseNameW(processHandle.getPointer(), Pointer.NULL, filename, filename.length);
        String name=new String(filename);
        System.out.println(name);
        if (name.endsWith("wwahost.exe")) { // Metro App
            // There is no stable API to get the current Metro app
            // But you can guestimate the name form the current directory of the process
            // To query this, see:
            // https://dev59.com/PGQo5IYBdhLWcg3wXeb5
        }

Linux / Unix / X11

使用 X11 存在三个问题:

  1. 由于网络透明性,完全来自不同机器的多个窗口可能会混合在同一个 X11 中。因此,在查询时,窗口所属进程的名称和 PID 在您所查询的机器上可能都没有意义。
  2. 大多数窗口管理器都有多个桌面。每个桌面上可以有不同的应用程序处于前台。
  3. 平铺式窗口管理器(例如 XMonad)没有前景窗口的概念。它们以一种方式排列所有窗口,使得每个窗口都同时处于前景。

在 X11 上,查询当前拥有焦点的窗口更为合理。

public interface XLib extends StdCallLibrary {
    XLib INSTANCE = (XLib) Native.loadLibrary("XLib", Psapi.class);

    int XGetInputFocus(X11.Display display, X11.Window focus_return, Pointer revert_to_return);
}

if(Platform.isLinux()) {  // Possibly most of the Unix systems will work here too, e.g. FreeBSD
        final X11 x11 = X11.INSTANCE;
        final XLib xlib= XLib.INSTANCE;
        X11.Display display = x11.XOpenDisplay(null);
        X11.Window window=new X11.Window();
        xlib.XGetInputFocus(display, window,Pointer.NULL);
        X11.XTextProperty name=new X11.XTextProperty();
        x11.XGetWMName(display, window, name);
        System.out.println(name.toString());
    }

Mac OS X

Mac OS X不关注窗口,而是关注应用程序。因此,询问当前活动的应用程序是有意义的。旧版本的Mac OS X提供多个桌面。新版本可以同时打开多个全屏应用程序。因此,您可能无法始终获得正确的答案。

    if(Platform.isMac()) {
        final String script="tell application \"System Events\"\n" +
                "\tname of application processes whose frontmost is tru\n" +
                "end";
        ScriptEngine appleScript=new ScriptEngineManager().getEngineByName("AppleScript");
        String result=(String)appleScript.eval(script);
        System.out.println(result);
    }

结论

当我尝试使用这段代码时,在最基本的情况下它是可行的。但是如果你想让这段代码运行得更加可靠,那么你需要添加很多细节处理。你需要自己决定是否值得这样做。

为了让代码完整,这里是我所使用的导入部分:

    import com.sun.jna.Native;
    import com.sun.jna.Platform;
    import com.sun.jna.Pointer;
    import com.sun.jna.platform.unix.X11;
    import com.sun.jna.platform.win32.Kernel32;
    import com.sun.jna.platform.win32.User32;
    import com.sun.jna.platform.win32.WinDef;
    import com.sun.jna.platform.win32.WinNT;
    import com.sun.jna.ptr.IntByReference;
    import com.sun.jna.win32.StdCallLibrary;

    import javax.script.ScriptEngine;
    import javax.script.ScriptEngineManager;
    import javax.script.ScriptException;

当然,您必须重新排列代码的各个部分。我使用了一个大类,其中接口在开头,然后其余部分都在一个大的主方法中。


嗨,Stefan,关于你在Linux/Unix代码示例中使用“Psapi.class”加载XLib的问题,我不明白为什么你要使用“Psapi.class”而不是自己的“XLib.class”接口进行加载。你能解释一下吗? - Alexander Plickat

11

很遗憾,没有适用于此的Java API。JVM并不知道它没有管理的窗口信息。您可能需要使用JNI并调用此函数。

[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();

MSDN链接

PS. 如果您需要获取窗口的标题,可以使用GetWindowText函数。

这篇文章提供了一些对您有用的JNI示例。


DllImport?看起来像是C#。 - Hovercraft Full Of Eels
@Hovercraft Full Of Eels 有一些库可以让你在Java中绑定DLL。不过这个语法可能是C#的。 - extraneon
1
是的,语法来自C#。我只是想展示DLL和函数签名。 - Bala R
这看起来是个不错的选择!我以前从没听说过JNI,似乎有点混淆。能否请你指明我需要导入/调用什么来使它在一个Java类中工作?如果你能写一个简单的类来实现GetWindowText函数就更好了。谢谢! - Daniel Loureiro
@Daniel,请查看该帖子中的示例。 - Bala R
3
就我个人来说,我认为使用JNA比JNI更好。它基于JNI,但使用起来要简单得多(我认为)。 - Hovercraft Full Of Eels

7
作为满载鳗鱼的Hovercraft所说,JNA 是你最好的选择。与JNI不同的是,你不需要为此编译任何C代码。
要获取进程名称:
  1. 调用GetForegroundWindow()以获取窗口句柄
  2. 调用GetWindowThreadProcessId()以确定哪个进程拥有它
  3. 调用OpenProcess()以使用PROCESS_QUERY_INFORMATION | PROCESS_VM_READ获取进程的句柄
  4. 调用GetModuleFileNameEx()以从句柄中获取进程名称。您还可以调用GetModuleBaseName()仅获取模块名称而不是完整路径。

Java中获取活动窗口信息的完整示例可在此处找到。

C代码可在这里找到。


好的,原始问题是针对Windows的,但是相同的方法应该适用于其他任何操作系统。对于Mac OS X,您可以尝试移植这个Python代码。 它应该能够让您开始调用哪些功能。 - kichik
赏金如此之高,希望能找到适用于Linux / Mac的答案。尽管如此,非常感谢您迄今为止的帮助。 :) - Ali
@ClickUpvote,这个网站上有其他答案告诉你如何在Mac和Linux上用python实现它;你只需要将代码移植到Java。 - Anya Shenanigans
令人难以置信,非常有帮助。 - PascalVKooten

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接