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?
CodePudding user response:
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);
}
CodePudding user response:
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