Reputation: 25929
I'm writing a StreamDeck plugin, which allows bringing specific processes window to the front. The idea is to be able to quickly switch to specific application with a press to a button (eg. switch to Teams call and hang it up with press of another button). Moreover, this is tool (mostly) for me, so please, no "another next application with forced annoying popups" comments.
I'm wondering, what would be the proper way of implementing this feature?
Windows may be brought into foreground with SetForegroundWindow
function, which has the following restrictions mentioned in the docs:
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.
Important thing: I am aware of the workarounds (namely, simulating pressing alt key, what actually produces weird results sometimes).
However, I don't want to workaround the problem, but solve it directly (if possible).
The catch is, that plugins for Stream Deck works as separate processes (actually, even separate executables) and communicate through web sockets, with all the consequences (eg. I may as well create windows if needed).
My question would be: what would be the proper way of activating a window from within my process? The problem is that entry point is not direct user input (in terms of what WinAPI treats as user input, what would probably solve the problem), but user input via websockets.
Edit: in response to comments.
I tried to implement the solution with UI automation and SetFocus worked, but the window was not brought to foreground.
The source (you can copy & paste to a new console application) is as following:
#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();
}
}
What am I doing wrong?
Upvotes: 0
Views: 1790
Reputation: 25929
The solution I finally used combines restoring window when it is minimized and then using UI automation API.
I suppose that the most credit goes to Simon Mourier, who proposed the solution in a comment.
Relevant parts of code follows:
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;
}
So far works all the time and contains no hacks and other tricks. Tested on Spotify, Notepad, Teams and Visual Studio.
Full source of the plugin is available on GitLab: https://gitlab.com/spook/StreamDeckSwitchTo.git
Upvotes: 1
Reputation: 2779
SetForegroundWindow
works best only when calling application is the foreground one. If button is owned by your process it should work without problem, but if button is owned by main StreamDeck process, you need somehow to instruct it to call SetForegroundWindow
with proper arguments (maybe feature request is needed).
For keyboard input RegisterHotKey
is recommended. When registered hot key combination is pressed the window processing WM_HOTKEY
gets special exemption (foreground love https://devblogs.microsoft.com/oldnewthing/20090226-00/?p=19013) so SetForegroundWindow
could be used to change foreground window.
In other cases, you could use workarounds, risking that in the future they could stop working.
SetForegroundWindow(hwnd);
if (GetForegroundWindow() != hwnd)
{
SwitchToThisWindow(hwnd, TRUE);
Sleep(2);
SetForegroundWindow(hwnd);
}
And another one, very annoying when composition is switched off.
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);
}
Upvotes: 1