如何从进程ID获取主窗口句柄?

71

如何从进程ID获取窗口句柄?

我想将该窗口置于前台。

在“Process Explorer”中可以正常工作。


7
如果你同时打开了两个Firefox窗口,哪一个是“主”窗口? 它们是相等的。Process Explorer似乎会选择最近获得焦点的那个窗口作为“主”窗口。 - Rob Kennedy
5
Windows没有维护“主窗口”的概念。它有顶级窗口、子窗口和拥有的窗口。任何进程都可以拥有零个或多个顶级窗口。除非您提供一个简明规范来确定“主窗口”,否则无法回答这个问题。 - IInspectable
7个回答

77

我查看了.NET如何确定主窗口。

我的研究表明,它也使用EnumWindows()

这段代码应该与.NET方法类似:

struct handle_data {
    unsigned long process_id;
    HWND window_handle;
};

HWND find_main_window(unsigned long process_id)
{
    handle_data data;
    data.process_id = process_id;
    data.window_handle = 0;
    EnumWindows(enum_windows_callback, (LPARAM)&data);
    return data.window_handle;
}

BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM lParam)
{
    handle_data& data = *(handle_data*)lParam;
    unsigned long process_id = 0;
    GetWindowThreadProcessId(handle, &process_id);
    if (data.process_id != process_id || !is_main_window(handle))
        return TRUE;
    data.window_handle = handle;
    return FALSE;   
}

BOOL is_main_window(HWND handle)
{   
    return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle);
}

你能解释一下 is_main_window 的逻辑吗?在我的测试中,我没有看到使用 if (data.process_id != process_id || !IsWindowVisible(handle)) 有什么区别。此外,这种方法需要微调以支持具有多个主窗口的进程 ID(例如 Web 浏览器)。 - Class Skeleton
5
GetWindow(handle, GW_OWNER) == 0检查窗口是否不是所有窗口(例如对话框或其他窗口)。IsWindowVisible(handle) 检查窗口是否可见且未隐藏(许多没有 GUI 的应用程序仍然有一个隐藏的窗口,甚至像在系统托盘中运行的配置应用程序这样的具有隐藏 GUI 的应用程序也有)。因此,如果窗口可见且没有所有者,则被视为“主窗口”,这足以描述大多数“主窗口”。 - Jason C
这是一个非常好的关于父窗口和所有者的解释,可能会为很多人澄清很多问题,有助于添加或删除此代码中的逻辑:https://blogs.msdn.microsoft.com/oldnewthing/20100315-00/?p=14613 简而言之,没有父窗口的窗口仍然可以有所有者,并且不是顶级窗口。 - Beeeaaar
这对于一些应用程序有效,但对于更复杂的事情可能会出现问题。一些应用程序(例如 MSO)有许多窗口根据此定义是“主要”的,而且不能安全地首先关闭。我认为检查窗口类名是获取大多数应用程序的“主”窗口的好方法。此外,请注意竞态条件。当您循环时,用户可能会关闭应用程序并破坏您的代码。 - sudo rm -rf slash

43

我不相信Windows(与.NET相对)提供了直接获取此信息的方法。

我所知道的唯一方法是使用EnumWindows()枚举所有顶层窗口,然后找出每个窗口属于哪个进程GetWindowThreadProcessID()。这听起来间接且效率低下,但它并没有你想象中那么糟糕 - 在典型情况下,您可能需要遍历十几个顶层窗口...


6
我如何知道哪个是主窗口? - Alexey Malistov
7
你描述得非常贴切,正是msdn文章链接所建议的内容,只不过用了少了1000个单词。 - hometoast
8
从MSDN上来看,EnumWindows函数不会枚举子窗口。 - Jerry Coffin
3
经验使得表达更简洁。我在那篇文章发表四年前就已经公开了这种方法。 - Jerry Coffin
1
参考一下,这是.NET检索主窗口句柄的方法:System.Diagnostics.MainWindowFinder.FindMainWindowEnumWindowsCallback。因此可以说,.NET也没有提供一个“直接的方式”。 - IInspectable

11

这是我基于顶部答案的纯Win32/C++解决方案。思路是将所有必需的内容包装到一个函数中,而无需外部回调函数或结构:

#include <utility>

HWND FindTopWindow(DWORD pid)
{
    std::pair<HWND, DWORD> params = { 0, pid };

    // Enumerate the windows using a lambda to process each window
    BOOL bResult = EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL 
    {
        auto pParams = (std::pair<HWND, DWORD>*)(lParam);

        DWORD processId;
        if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second)
        {
            // Stop enumerating
            SetLastError(-1);
            pParams->first = hwnd;
            return FALSE;
        }

        // Continue enumerating
        return TRUE;
    }, (LPARAM)&params);

    if (!bResult && GetLastError() == -1 && params.first)
    {
        return params.first;
    }

    return 0;
}

3
为避免在应用程序有两个窗口的情况下提前退出循环,您可能需要仔细检查窗口是否没有所有者:使用 if (GetWindowThreadProcessId(hwnd, &processId) && processId == pParams->second && GetWindow(hwnd, GW_OWNER) == 0)。请注意不改变原意,并使翻译易于理解。 - Pep

11

这里可能存在误解。在 .Net 中,WinForms 框架会自动将第一个创建的窗口(例如,Application.Run(new SomeForm()))指定为 MainWindow。然而,win32 API 并不认可进程级别的“主窗口”概念。消息循环完全能够处理尽可能多的“主”窗口,只要系统和进程资源允许你创建即可。因此,你的进程没有“主窗口”。一般情况下,你只能使用 EnumWindows() 获取给定进程上所有非子窗口并尝试使用一些启发式方法来确定你想要的那个窗口。幸运的是,大多数进程通常只有单个“主”窗口在运行,所以大多数情况下应该可以得到良好的结果。


3
实际上,.NET 在第一次访问System.Diagnostics.Process.MainWindowHandle属性时缓存窗口句柄。窗口句柄不是预先存储的。它使用与Jerry Coffin在他的答案中概述的相同算法进行评估。 - IInspectable
是的,你说得对。我不知道我从哪里得到了那个说法 - 可能只是根据经验假设的,尽管窗口的枚举顺序不太可能得到保证。 - Dathan

2

1
作为对Hiale解决方案的扩展,您可以提供一个不同或修改过的版本,支持具有多个主窗口的进程。
首先,修改结构以允许存储多个句柄:
struct handle_data {
    unsigned long process_id;
    std::vector<HWND> handles;
};

第二步,修改回调函数:
BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM lParam)
{
    handle_data& data = *(handle_data*)lParam;
    unsigned long process_id = 0;
    GetWindowThreadProcessId(handle, &process_id);
    if (data.process_id != process_id || !is_main_window(handle)) {
        return TRUE;
    }
    // change these 2 lines to allow storing of handle and loop again
    data.handles.push_back(handle);
    return TRUE;   
 }

最后,在主函数中修改返回值:

std::vector<HWD> find_main_window(unsigned long process_id)
{
    handle_data data;
    data.process_id = process_id;
    EnumWindows(enum_windows_callback, (LPARAM)&data);
    return data.handles;
}

当你在堆栈帧之间传递控制时,中间的所有帧都需要知道这个笑话。 - IInspectable

-1

只是为了确保您不会混淆tid(线程ID)和pid(进程ID):

DWORD pid;
DWORD tid = GetWindowThreadProcessId( this->m_hWnd, &pid);

15
这是OP所需求的反函数。 - Sebastian

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