这个 Windows 程序应该是一个小型终端实用程序,可以整齐地堆叠正在运行的应用程序的窗口。
用户传入他们想要执行此操作的可执行应用程序的名称,程序会查找可执行名称与用户输入紧密匹配的可能进程。用户在询问时确认他们选择的应用程序,并且程序堆叠这些窗口(不给它们焦点)
问题
该程序的问题在于,它会调出并堆叠应用程序的所有窗口,包括用户不打算与之交互的窗口(有时,这些窗口在应用程序启动时会隐藏)。这会导致桌面出现问题,并且我不确定如何区分面向用户的窗口和其他窗口。是否有一组常规窗口通常使用的属性标志需要测试?
见下图。这些窗口并不像普通窗口那样真正具有交互性,并且具有奇怪的属性。
请原谅代码的长度,但它会在Windows环境下编译。
// C++ standard library
#include <cassert>
#include <cstdio>
#include <fcntl.h>
#include <io.h>
#include <iostream>
#include <set>
#include <string>
#include <vector>
// Windows API
#include <Windows.h>
// Windows API helpers
#include <TlHelp32.h>
/**
* @brief Takes a snapshot of all processes in the system and returns a vector containing each process entry.
*
*/
static std::vector<PROCESSENTRY32> getAllProcesses( ) {
// Take the snapshot.
HANDLE snapshotHandle = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 ); // @Todo Handle errors.
assert( snapshotHandle != INVALID_HANDLE_VALUE );
// Get all the processes in the snapshot.
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof( PROCESSENTRY32 ); // Must initialize this member before iterating.
bool noErrors = Process32First( snapshotHandle, &processEntry ); // @Todo Handle errors.
assert( noErrors ); // For now, assume this always succeeds the first time.
std::vector<PROCESSENTRY32> result;
result.push_back( processEntry ); // Append the first process entry.
// Query for the next process entry.
DWORD errorCode = ERROR_SUCCESS;
if ( !Process32Next( snapshotHandle, &processEntry ) ) {
errorCode = GetLastError( );
}
while ( errorCode != ERROR_NO_MORE_FILES ) {
result.push_back( processEntry );
errorCode = ERROR_SUCCESS;
if ( !Process32Next( snapshotHandle, &processEntry ) ) {
errorCode = GetLastError( );
}
}
CloseHandle( snapshotHandle );
return result;
}
/**
* @brief Returns the name of the executable file for a process.
*
*/
static std::wstring getProcessName( PROCESSENTRY32 const& p ) {
std::wstring result;
size_t const nameLength = wcslen( p.szExeFile );
for ( size_t i = 0; i < nameLength; ++i ) {
result.push_back( p.szExeFile[i] );
}
return result;
}
/**
* @brief Returns the ID of the process that opened a given window.
*
*/
static DWORD getWindowProcessID( HWND windowHandle ) {
DWORD processID = 0;
DWORD threadID = GetWindowThreadProcessId( windowHandle, &processID );
if ( threadID == 0 ) {
std::abort( );
}
return processID;
}
/**
* @brief Determines whether a given window was opened by a given process.
*
* @return true if the process opened the window.
* @return false if the process did not open the window.
*/
static bool windowOpenedByProcess( HWND windowHandle, DWORD processID ) {
return getWindowProcessID( windowHandle ) == processID;
}
/**
* @brief Returns a vector containing the IDs of the given processes.
*
*/
static std::vector<DWORD> getProcessIDs( std::vector<PROCESSENTRY32> const& processes ) {
std::vector<DWORD> ids;
for ( const PROCESSENTRY32& i : processes ) {
ids.push_back( i.th32ProcessID );
}
return ids;
}
/**
* @brief Determines whether a given window was opened by any of the given processes.
*
* @return true if the window was opened by one of the processes.
* @return false if the window was not opened by any of the processes.
*/
static bool windowOpenedByProcessAnyOf( HWND windowHandle, std::vector<PROCESSENTRY32> const& processes ) {
std::vector<DWORD> processIDs = getProcessIDs( processes );
for ( const DWORD& i : processIDs ) {
if ( windowOpenedByProcess( windowHandle, i ) ) {
return true;
}
}
return false;
}
/**
* @brief Returns a string where all alphabetic characters in the given string have been demoted to lowercase.
*
*/
template <class Container, class Element> Container toLower( Container const& c ) {
Container result;
for ( Element const& e : c ) {
if ( isalpha( e ) ) {
int const lowerAsInt = tolower( e );
// assert((lowerAsInt >= CHAR_MIN) && (lowerAsInt <= CHAR_MAX));
result.append( 1, static_cast<Element>( lowerAsInt ) );
} else {
result.append( 1, e );
}
}
return result;
}
/**
* @brief Takes a vector of processes and removes the ones whose executable name does not contain the search key as a
* substring.
*
*/
static void filterProcessesNotContainingName( std::vector<PROCESSENTRY32>& processes, std::wstring const& searchFor ) {
std::wstring searching = toLower<std::wstring, wchar_t>( searchFor ); // For case-insensitive search.
size_t count = processes.size( );
for ( size_t i = 0; count > 0; --count ) {
std::wstring processName = toLower<std::wstring, wchar_t>( getProcessName( processes.at( i ) ) );
size_t foundAt = processName.find( searching );
if ( foundAt != std::wstring::npos ) {
++i; // Found a loose match. Move on to the next one.
} else {
processes.erase( processes.begin( ) + i ); // No match. Remove this one.
}
}
}
/**
* @brief Takes a vector of processes and removes the ones whose executable name does not exactly match the filtering
* key.
*
*/
static void filterProcessesNotMatchingName( std::vector<PROCESSENTRY32>& processes, std::wstring const& searchFor ) {
size_t count = processes.size( );
for ( size_t i = 0; count > 0; --count ) {
if ( getProcessName( processes.at( i ) ) == searchFor ) {
++i;
} else {
processes.erase( processes.begin( ) + i );
}
}
}
/**
* @brief Returns a vector containing the executable names of all given processes.
*
*/
static std::vector<std::wstring> getProcessNames( std::vector<PROCESSENTRY32> const& allProcesses ) {
std::vector<std::wstring> result;
for ( PROCESSENTRY32 const& i : allProcesses ) {
result.push_back( getProcessName( i ) );
}
return result;
}
/**
* @brief Converts a narrow string to a wide string.
*
*/
static inline std::wstring narrowToWideString( std::string const& as ) {
// deal with trivial case of empty string
if ( as.empty( ) )
return std::wstring( );
// determine required length of new string
size_t reqLength = ::MultiByteToWideChar( CP_UTF8, 0, as.c_str( ), ( int )as.length( ), 0, 0 );
// construct new string of required length
std::wstring ret( reqLength, L'\0' );
// convert old string to new string
::MultiByteToWideChar( CP_UTF8, 0, as.c_str( ), ( int )as.length( ), &ret[0], ( int )ret.length( ) );
// return new string ( compiler should optimize this away )
return ret;
}
/**
* @brief Prints a container type as a numbered list to standard output.
*
*/
static void printNumberedList( std::set<std::wstring> const& container ) {
int n = 1;
for ( auto const& i : container ) {
std::wcout << " [" << n << "]\t" << i << "\n";
++n;
}
}
/**
* @brief Queries the user to select a running application from a list of possible applications.
*
* @note This function makes the assumption that similarly named executable names all belong to the same application.
*/
static void userSelectApplication( std::vector<PROCESSENTRY32>& processes ) {
std::set<std::wstring> uniqueProcessNames;
for ( std::wstring const& i : getProcessNames( processes ) ) {
uniqueProcessNames.insert( i );
}
std::wcout << "Found " << uniqueProcessNames.size( ) << " possible application(s)..." << std::endl << std::endl;
printNumberedList( uniqueProcessNames );
std::wcout << std::endl;
std::wcout << "Please select the desired application..." << std::endl;
std::wcout << "> ";
// @Todo Handle out-of-bounds access.
size_t selection = 0;
std::cin >> selection;
std::vector<std::wstring> uniqueProcessNamesAsVector{ uniqueProcessNames.begin( ), uniqueProcessNames.end( ) };
filterProcessesNotMatchingName( processes, uniqueProcessNamesAsVector.at( selection - 1 ) );
}
/**
* @brief Given an executable name as a loose search key, returns a vector of processes whose executable name matches.
*
* @details First, a loose search is formed. If the loose succeeds, the user is prompted to select the exact executable.
* If the loose search fails, then there were no possible search results for the given executable name.
*
*/
static std::vector<PROCESSENTRY32> findApplication( std::wstring const& executableName ) {
std::vector<PROCESSENTRY32> processes = getAllProcesses( );
filterProcessesNotContainingName( processes, executableName );
if ( !processes.empty( ) ) {
userSelectApplication( processes );
}
return processes;
}
/**
* @brief Returns the title of a window.
*
* @note The returned string may be empty if the window has no text for the title or for other reasons.
*/
static std::wstring getWindowTitle( HWND windowHandle ) {
SetLastError( 0 );
int titleLength = GetWindowTextLength( windowHandle );
DWORD error = GetLastError( );
if ( titleLength == 0 && GetLastError( ) != 0 ) {
std::abort( );
}
wchar_t* title = new wchar_t[titleLength + 1];
int copied = GetWindowText( windowHandle, title, titleLength + 1 );
if ( copied == 0 && GetLastError( ) != 0 ) {
delete[] title;
std::abort( );
}
assert( copied == titleLength );
std::wstring result{ title };
delete[] title;
return result;
}
/**
* @brief Returns a vector of window titles as strings for a vector of windows.
*
*/
static std::vector<std::wstring> getWindowTitles( std::vector<HWND> const& windowHandles ) {
std::vector<std::wstring> result;
for ( auto const& i : windowHandles ) {
result.push_back( getWindowTitle( i ) );
}
return result;
}
/**
* @brief Callback used to enumerate all top-level windows.
*
*/
static BOOL CALLBACK enumWindowsCallback( HWND windowHandle, LPARAM parameter ) {
std::vector<HWND>* pHandles = reinterpret_cast<std::vector<HWND>*>( parameter );
( *pHandles ).push_back( windowHandle );
return true;
}
/**
* @brief Takes a vector of handles to windows and removes those whose window was not opened by a given process.
*
*/
static void filterWindowsNotOpenedByProcesses( std::vector<HWND>& windowHandles,
std::vector<PROCESSENTRY32> const& processes ) {
size_t count = windowHandles.size( );
for ( size_t i = 0; count > 0; --count ) {
if ( windowOpenedByProcessAnyOf( windowHandles.at( i ), processes ) ) {
++i;
} else {
windowHandles.erase( windowHandles.begin( ) + i );
}
}
}
/**
* @brief Takes a vector of handles to windows and removes those that have no text for their title.
*
*/
static void filterUnnamedWindows( std::vector<HWND>& windowHandles ) {
size_t count = windowHandles.size( );
for ( size_t i = 0; count > 0; --count ) {
if ( !getWindowTitle( windowHandles.at( i ) ).empty( ) ) {
++i;
} else {
windowHandles.erase( windowHandles.begin( ) + i );
}
}
}
/**
* @brief Takes a vector of handles to windows and puts each owned window in a "show" state, but does not activate them.
*
*/
static void showAllWindows( std::vector<HWND>& windowHandles ) {
for ( auto& i : windowHandles ) {
ShowWindow( i, SW_SHOWNOACTIVATE );
}
}
/**
* @brief Takes a vector of handles to windows and stacks all owned windows over top one another.
*
*/
static BOOL stackWindows( std::vector<HWND>& windowHandles ) {
int const windowLength = 500;
int const windowHeight = 300;
// POINT windowSize = { windowLength, windowHeight };
int x = 10;
int y = 10;
// POINT startPosition = { x, y };
int const incLeft = 30;
int const incDown = 20;
UINT const options = SWP_NOZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE;
BOOL noErrors = true;
for ( auto& i : windowHandles ) {
noErrors = noErrors && SetWindowPos( i, HWND_TOP, x, y, windowLength, windowHeight, options );
x += incLeft;
y += incDown;
}
return noErrors;
}
int main( int argc, char* argv[] ) {
if ( argc == 1 ) {
std::cout << "Not enough arguments.\n";
return EXIT_SUCCESS;
} else if ( argc > 2 ) {
std::cout << "Too many arguments.\n";
return EXIT_SUCCESS;
}
_setmode( _fileno( stdout ), _O_U16TEXT ); // Set output to unicode mode for wide text strings.
std::string const userInput{ argv[1] };
std::wstring const executableName = narrowToWideString( userInput );
std::vector<PROCESSENTRY32> processes = findApplication( executableName );
if ( processes.empty( ) ) {
std::cout << "No applications found.\n";
return EXIT_SUCCESS;
}
// Enumerate all top-level windows.
std::vector<HWND> windowHandles;
EnumWindows( &enumWindowsCallback, reinterpret_cast<LPARAM>( &windowHandles ) );
filterWindowsNotOpenedByProcesses( windowHandles, processes );
filterUnnamedWindows( windowHandles ); // Weed out windows not likely to be user-facing, interactive windows.
showAllWindows( windowHandles );
stackWindows( windowHandles );
return 0;
}