What you’re trying to do requires multiple, nontrivial P/Invoke calls to the WinAPI:
# Helper type for various window-related WinAPI functions.
Add-Type -Namespace Util -Name WinApi -MemberDefinition @'
// The callback delegate, which receives:
// - the hWnd being enumerated
// - a custom LPARAM value passed on invocation to EnumWindows()
// The delegate must return $true to keep enumerating; in other words: $false stops the enumeration.
// Note the custom ArrayList parameter in lieu of an IntPtr (LPARAM) (using a generic list is NOT an option, because generic types cannot be marshalled).
public delegate bool EnumWindowsProc(IntPtr hWnd, System.Collections.ArrayList lParam);
// The EnumWindows() API function that enumerates *top-level windows*.
// It too uses a custom ArrayList parameter instead of the IntPtr (LPARAM), which the callback receives.
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, System.Collections.ArrayList lParam);
// Get the class name of a window by its handle.
[DllImport("user32.dll")]
public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder classNameBuf, int nMaxCount);
// Cascade the specified window(s).
[DllImport("user32.dll")]
public static extern ushort CascadeWindows(IntPtr hwndParent, uint wHow, IntPtr lpRect, uint cKids, IntPtr[] lpKids);
// Show the specified window.
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
// Make the specified window the foreground window, if allowed (see below).
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
// Allow making the windows of other processes the foreground window
// for the remainder of the session.
[DllImport("user32.dll", EntryPoint="SystemParametersInfo")]
static extern bool SystemParametersInfo_Set_UInt32(uint uiAction, uint uiParam, UInt32 pvParam, uint fWinIni);
public static void AllowWindowActivation()
{
if (! SystemParametersInfo_Set_UInt32(0x2001 /* SPI_SETFOREGROUNDLOCKTIMEOUT */, 0, 0 /* timeout in secs */, 0 /* non-persistent change */)) {
throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error(), "Unexpected failure calling SystemParametersInfo() with SPI_SETFOREGROUNDLOCKTIMEOUT");
}
}
'@
# Get all Notepad windows, on the assumption that their window class name is 'Notepad'.
[System.Collections.ArrayList] $matchingHwnds = @()
$null = [Util.WinApi]::EnumWindows(
{ # delegate (callback function)
param([intptr] $hWnd, [System.Collections.ArrayList] $param)
$sb = [System.Text.StringBuilder]::new(1024)
$null = [Util.WinApi]::GetClassName($hWnd, $sb, $sb.Capacity-1)
if ($sb.ToString() -eq 'Notepad') {
$param.Add($hWnd)
}
return $true # continue enumerating
},
$matchingHwnds # the array list to pass as custom data
)
# Reverse the matching window handles (if any).
[IntPtr[]] $matchingHwndsInReverse = [Linq.Enumerable]::Reverse([IntPtr[]] $matchingHwnds)
# Make sure all windows are restored (non-minimized, non-maximized), in reverse order.
foreach ($hwnd in $matchingHwndsInReverse) {
$null = [Util.WinAPI]::ShowWindow($hwnd, 1)
}
# Cascade them.
$null = [Util.WinAPI]::CascadeWindows([IntPtr]::Zero, 0, [IntPtr]::Zero, $matchingHwnds.Count, $matchingHwnds)
# Activate them in reverse order.
# First, enable cross-process window activation...
[Util.WinAPI]::AllowWindowActivation()
# ... then activate them.
foreach ($hwnd in $matchingHwndsInReverse) {
$null = [Util.WinAPI]::SetForegroundWindow($hwnd)
}
As for what you tried:
Shell.Application.CascadeWindows()
indeed by design invariably cascades all (presumably non-minimized only) windows – you cannot constrain it to only a given application’s windows.- Additionally, this method seemingly no longer works as of Windows 11; similarly, the cascading feature has been removed from the GUI (you used to be able to right-click on the taskbar and select
Cascade windows
).
- Additionally, this method seemingly no longer works as of Windows 11; similarly, the cascading feature has been removed from the GUI (you used to be able to right-click on the taskbar and select
What you presumably meant was this – but it wouldn’t work on Windows 11, with the new, UWP implementation of Notepad:
(Get-Process | Where {$_.MainWindowTitle -and $_.Description -like '*note*' }).MainWindowTitle # Simpler alternative: @((Get-Process notepad).MainWindowTitle) -ne $null
On Windows 11, with the new UWP implementation of Notepad.exe, only the title of a single Notepad window – namely the currently or most recently active one – is reflected in the
.MainWindowTitle
property of the Notepad processes, so that there’s no one-to-one relationship between processes and visible windows.
Therefore, a WinAPI-based solution, as shown above, is required, because it:
allows you to find top-level windows by class name, independently of examining processes.
allows you to cascade only a given set of windows (though you must activate them separately).