使用 WinAPI 激活 window 的正确方法

Proper way of activating a window using WinAPI

我正在编写一个 StreamDeck 插件,它允许将特定进程 window 放在前面。这个想法是能够通过按下按钮快速切换到特定应用程序(例如,切换到 Teams 呼叫并通过按下另一个按钮挂断电话)。此外,这对我来说(主要)是工具,所以,不要评论“下一个带有强制烦人弹出窗口的应用程序”。

我想知道,实现此功能的正确方法是什么?

Windows 可以用 SetForegroundWindow 函数调到前台,它在文档中有以下限制:

The system restricts which processes can set the foreground window. A process can set the foreground window only if one of the following conditions is true:

  • The process is the foreground process.
  • The process was started by the foreground process.
  • The process received the last input event.
  • There is no foreground process.
  • The process is being debugged.
  • The foreground process is not a Modern Application or the Start Screen.
  • The foreground is not locked (see LockSetForegroundWindow).
  • The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
  • No menus are active.

An application cannot force a window to the foreground while the user is working with another window. Instead, Windows flashes the taskbar button of the window to notify the user.

A process that can set the foreground window can enable another process to set the foreground window by calling the AllowSetForegroundWindow function. The process specified by dwProcessId loses the ability to set the foreground window the next time the user generates input, unless the input is directed at that process, or the next time a process calls AllowSetForegroundWindow, unless that process is specified.

重要事项:我知道解决方法(即模拟按下 alt 键,有时实际会产生奇怪的结果)。

但是,我不想解决这个问题,而是直接解决它(如果可能的话)

要注意的是,Stream Deck 的插件作为单独的进程(实际上,甚至是单独的可执行文件)工作,并通过网络套接字进行通信,并带来所有后果(例如,如果需要,我也可以创建 windows ).

我的问题是:从我的进程中激活window的正确方法是什么?问题是入口点不是 直接 用户输入(就 WinAPI 将什么视为用户输入而言,这可能会解决问题),而是通过 websockets 的用户输入。


编辑: 回复评论。

我尝试使用 UI 自动化来实施解决方案,并且 SetFocus 起作用了,但是 window 没有被带到前台。

来源(您可以复制并粘贴到新的控制台应用程序)如下:

#include <iostream>
#include <string>
#include <windows.h>
#include <tlhelp32.h>
#include <cstdio>
#include <wctype.h>
#include <locale>
#include <codecvt>
#include <WinUser.h>
#include <vector>
#include <algorithm>
#include <uiautomationclient.h>

/// <summary>
/// Contains information about single process and associated windows
/// </summary>
struct process_data
{
    unsigned long process_id;
    std::vector<HWND> window_handles;
};

/// <summary>
/// Compares two wide strings case insensitive
/// </summary>
bool equals_case_insensitive(const wchar_t* a, const wchar_t* b)
{
    while (*a != 0 && *b != 0)
    {
        if (towlower(*a++) != towlower(*b++))
            return false;
    }

    return *a == 0 && *b == 0;
}

/// <summary>
/// Checks, if given window is main window of the process
/// </summary>
bool is_main_window(HWND handle)
{
    return GetWindow(handle, GW_OWNER) == (HWND)0 && IsWindowVisible(handle);
}

/// <summary>
/// Callback used for enumerating windows per processes
/// </summary>
BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM lParam)
{
    std::vector<process_data>& data = *(std::vector<process_data>*)lParam;
    unsigned long process_id = 0;
    GetWindowThreadProcessId(handle, &process_id);

    int i;
    for (i = 0; i < data.size() && data[i].process_id != process_id; i++);

    if (i < data.size())
    {
        if (is_main_window(handle))
        {
            data[i].window_handles.push_back(handle);
        }
    }

    return TRUE;
}

/// <summary>
/// Finds main windows for given processes
/// </summary>
std::vector<process_data> find_windows(std::vector<DWORD> process_ids)
{
    // Creates process_data entries
    std::vector<process_data> data;
    for (DWORD processId : process_ids)
    {
        process_data processData;
        processData.process_id = processId;
        data.push_back(processData);
    }

    // Collects windows of given processes
    EnumWindows(enum_windows_callback, (LPARAM)&data);

    // Removes processes without any windows
    int i = 0;
    while (i < data.size())
    {
        if (data[i].window_handles.size() == 0)
            data.erase(data.begin() + i);
        else
            i++;
    }

    // Sorts processes by their IDs (to provide some
    // way of ordering processes)
    std::sort(data.begin(), data.end(), [](process_data& first, process_data& second) { return first.process_id - second.process_id; });

    // For each process
    for (process_data& process : data)
    {
        // Sorts windows by their handles (again, to
        // provide some way of ordering them)
        std::sort(process.window_handles.begin(), process.window_handles.end(), [](HWND& first, HWND& second) {

            long long firstValue = (long long)first;
            long long secondValue = (long long)second;

            if (firstValue > secondValue)
                return 1;
            else if (firstValue < secondValue)
                return -1;
            else
                return 0;
            });
    }

    return data;
}

/// <summary>
/// Finds all process IDs, which executable name matches
/// given name (eg. notepad.exe)
/// </summary>
std::vector<DWORD> find_process_ids(const std::wstring& processName)
{
    // Collects information about running processes
    PROCESSENTRY32 processInfo;
    processInfo.dwSize = sizeof(processInfo);
    std::vector<DWORD> processIDs;

    HANDLE processesSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (processesSnapshot == INVALID_HANDLE_VALUE)
        return processIDs;

    // Collects all those, which executable name matches
    // one given in the parameter
    Process32First(processesSnapshot, &processInfo);
    if (equals_case_insensitive(processName.c_str(), processInfo.szExeFile))
    {
        processIDs.push_back(processInfo.th32ProcessID);
    }

    while (Process32Next(processesSnapshot, &processInfo))
    {
        if (equals_case_insensitive(processName.c_str(), processInfo.szExeFile))
        {
            processIDs.push_back(processInfo.th32ProcessID);
        }
    }

    CloseHandle(processesSnapshot);
    return processIDs;
}

int main()
{
    // Enumerate all matching process IDs
    std::vector<DWORD> processIDs = find_process_ids(L"notepad.exe");

    // We need at least one
    if (processIDs.size() == 0)
        return false;

    // Find windows for found processes
    std::vector<process_data> windows = find_windows(processIDs);

    // We need at least one process with window
    if (windows.size() == 0)
        return false;

    HWND handle = windows[0].window_handles[0];

    IUIAutomation* pAutomation;

    CoInitialize(nullptr);

    HRESULT hr = CoCreateInstance(__uuidof(CUIAutomation), NULL, CLSCTX_INPROC_SERVER, __uuidof(IUIAutomation), (void**)&pAutomation);
    if (SUCCEEDED(hr)) {
        printf("got IUIAutomation\r\n");

        IUIAutomationElement* window = nullptr;
        if (SUCCEEDED(pAutomation->ElementFromHandle(handle, &window)))
        {
            if (SUCCEEDED(window->SetFocus()))
            {
                std::cout << "Success" << std::endl;
            }

            window->Release();
        }

        pAutomation->Release();
    }
}

我做错了什么?

SetForegroundWindow 仅当调用应用程序是前台应用程序时效果最佳。如果按钮由您的进程拥有,它应该可以正常工作,但如果按钮由主 StreamDeck 进程拥有,您需要以某种方式指示它使用适当的参数调用 SetForegroundWindow(可能需要功能请求)。

建议使用键盘输入RegisterHotKey。当按下已注册的热键组合时,window 处理 WM_HOTKEY 获得特殊豁免(前景爱 https://devblogs.microsoft.com/oldnewthing/20090226-00/?p=19013),因此 SetForegroundWindow 可用于更改前景 window。

在其他情况下,您可以使用变通办法,冒着将来它们可能停止工作的风险。

SetForegroundWindow(hwnd);
if (GetForegroundWindow() != hwnd)
{
    SwitchToThisWindow(hwnd, TRUE);
    Sleep(2);
    SetForegroundWindow(hwnd);
}

还有一个,关闭合成时非常烦人。

SetForegroundWindow(hwnd);
if (GetForegroundWindow() != hwnd)
{
    BOOL flag = TRUE;
    DwmSetWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, &flag, sizeof(flag));
    ShowWindow(hwnd, SW_MINIMIZE);
    ShowWindow(hwnd, SW_RESTORE);
    flag = FALSE;
    DwmSetWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, &flag, sizeof(flag));
    SetForegroundWindow(hwnd);
}

我最终使用的解决方案结合了最小化时恢复window然后使用UI自动化API。

我认为最大的功劳归功于 Simon Mourier,他在评论中提出了解决方案。

相关部分代码如下:

int main(int argc, const char* const argv[])
{
    if (!SUCCEEDED(CoInitialize(nullptr)))
    {
        return 1;
    }
    // (...)
}

SwitchToPlugin::SwitchToPlugin()
{
    if (!SUCCEEDED(CoCreateInstance(__uuidof(CUIAutomation), NULL, CLSCTX_INPROC_SERVER, __uuidof(IUIAutomation), (void**)&(this->uiAutomation))))
    {
        throw new std::exception("Failed to create instance of UI automation!");
    }   
}

/// <summary>
/// Brings given window to the front
/// </summary>
bool SwitchToPlugin::bring_to_front(HWND hWnd)
{
    bool result = false;

    // First restore if window is minimized

    WINDOWPLACEMENT placement{};
    placement.length = sizeof(placement);

    if (!GetWindowPlacement(hWnd, &placement))
        return false;

    bool minimized = placement.showCmd == SW_SHOWMINIMIZED;
    if (minimized)
        ShowWindow(hWnd, SW_RESTORE);

    // Then bring it to front using UI automation

    IUIAutomationElement* window = nullptr;
    if (SUCCEEDED(uiAutomation->ElementFromHandle(hWnd, &window)))
    {
        if (SUCCEEDED(window->SetFocus()))
        {
            result = true;
        }

        window->Release();
    }

    return result;
}

到目前为止一直有效,不包含黑客和其他技巧。在 Spotify、Notepad、Teams 和 Visual Studio.

上测试

GitLab 上提供了插件的完整源代码:https://gitlab.com/spook/StreamDeckSwitchTo.git