public/Position-ExplorerWindow.ps1
function Position-ExplorerWindow { <# .SYNOPSIS Opens, resizes, and arranges multiple Explorer Windows at specified paths in a grid fashion to fit a screen, or multiple screens. .DESCRIPTION This script / module has the ability to quickly open multiple Explorer Windows at specified paths in an grid, arranging them to fit a screen, or multiple screens (no limit, really), in a predictable and orderly fashion. All you have to do is to add the folders into the script config, and run the script, and you get a grid of Explorer windows. This is most useful when working with half a dozen or more folders simultaneously, because it saves time and effort rearranging and fitting Explorer Windows into a nice and orderly fashion, so that they can be easily accessed. Background: An Explorer window cannot be opened at a specified coordinate on the screen through its command line. Because the author could not find any working solution that could conveniently open multiple Explorer windows in specified folders arranged in a predictable and orderly fashion on the screen, there had to be a tool that could do this. .PARAMETER Paths # Defines the paths (folders) the Explorer windows should show. # Enter one path per line. Edit between the single-quotes. # E.g. $Paths = ' # D:\My Data Folder\Data1 # D:\My Data Folder\Data2 # D:\My Data Folder\Data3 # D:\My Data Folder\Data4 # \\MYSERVER\public # \\192.168.0.1\\share # ' .PARAMETER ModeEasy # Simple Mode. In this mode, most defaults are used. # If you hate configurations, simply use this with -Paths .PARAMETER DestinationScreenWidth # Resolution of the Destination Screen (think of this as block of pixels, and not necesarily a Monitor's resolution) where the Explorer windows will reside. # For Single-Monitor setups, in most cases should match your Monitor's resolution. # For Multi-Monitor setups, you may also think of this a pooling of a block of pixels spanning your screen(s). You may use 3840 x 1080 to pool multiple monitor pixels together, or use 640 x 480 to select a smaller pool .PARAMETER DestinationScreenHeight # Resolution of the Destination Screen (think of this as block of pixels, and not necesarily a Monitor's resolution) where the Explorer windows will reside. # For Single-Monitor setups, in most cases should match your Monitor's resolution. # For Multi-Monitor setups, you may also think of this a pooling of a block of pixels spanning your screen(s). You may use 3840 x 1080 to pool multiple monitor pixels together, or use 640 x 480 to select a smaller pool .PARAMETER DestinationMonitor # Physical position of the Destination Monitor where the Explorer windows will open # NOTE: # This is ignored if you have only 1 monitor. # Possible values: 'M', L', 'R', 'T', 'B' # E.g. 'M' - Destination Monitor is the Main Monitor # E.g. 'L' - Destination Monitor is to the left of the Main Monitor # E.g. 'R' - Destination Monitor is to the right of the Main Monitor # E.g. 'T' - Destination Monitor is to the top of the Main Monitor # E.g. 'B' - Destination Monitor is to the bottom of the Main Monitor # Default: 'M' .PARAMETER Rows # Define the number of rows of Explorer instances # E.g. 4 - a maximum of four explorer instances will be stacked vertically in a column. The 5th-8th windows will be stacked on the next column to the right of the previous column. And so on. # Default: 4 .PARAMETER Cols # Define the number of columns of Explorer instances # If a value greater than 1 is specified, columns of x windows will stack horizontally (where x is a defined in $Rows) # E.g. A value of 2 means that 2 columns of x explorer instances will be stacked horizontally # Default: 2 .PARAMETER OffsetLeft # How many pixels left/right the Explorer instances should be shifted from the Top-Left Corner(0,0) of the Destination Monitor. Useful in the case of multiple-monitor setups. # Best to be left as default (left-most of Destination screen) # E.g. Single Monitor setups: # 0 positions the windows on the Main monitor, starting from its leftmost edge # E.g. Multi-Monitor setups: # 0 positions the windows on the Destination monitor, starting from its leftmost edge. # x the windows the windows on the Destination Monitor, x pixels right of its leftmost edge. # -x positions the windows on the Destination Monitor, x pixels left of its leftmost edge. # Default: 0 .PARAMETER OffsetTop # How many pixels up/down the Explorer instances should be shifted from the Top-Left Corner(0,0) of the Destination Monitor. Useful in the case of multiple-monitor setups. # Best to be left as default (The very top of destination screen) # E.g. Single Monitor setups: # 0 positions the windows on the Main monitor, starting from its topmost edge # E.g. Multi-Monitor setups: # 0 positions the windows on the Destination monitor, starting from its topmost edge. # y the windows the windows on the Destination Monitor, x pixels down of its topmost edge. # -y positions the windows on the Destination Monitor, x pixels up of its topmost edge. # Default: 0 .PARAMETER Flow # Arrangement of Explorer Windows # Whether windows should flow left-to-right, or top-down fashion # 'X' - Left-to-Right fashion. # --------- # | 1 | 2 | # --------- # | 3 | 4 | # --------- # 'Y' - Top-Down fashion # --------- # | 1 | 3 | # --------- # | 2 | 4 | # --------- # Default: 'Y' .PARAMETER DebugLevel # Debug level # 0 - Off # 1 - On .EXAMPLE Example 1a: This opens 4 windows: all 4 windows stacked vertically, occupying a total of half your Main full-HD Screen. Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 1920 -DestinationScreenHeight 1080 -DestinationMonitor 'M' -Rows 4 -Cols 2 -Flow 'Y' Example 1b: This is the same as Example 1, except instead of stacking vertically, windows flow in a zig-zag fashion: the first 2 windows are stacked horizontally in one row, then the next 2 are stacked horintally on the next row below. Each window's width is 1/2 the screen's width, and height 1/4 the screen's height. Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 1920 -DestinationScreenHeight 1080 -DestinationMonitor 'M' -Rows 4 -Cols 2 -Flow 'X' Example 2: This opens 4 windows: 3 windows stacked vertically on the left half of your Main full-HD Screen, and 1 window on the top occupying 1/3 of the right half of your Main full-HD Screen. Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 1920 -DestinationScreenHeight 1080 -DestinationMonitor 'M' -Rows 3 -Cols 2 -OffsetLeft 0 -OffsetTop 0 -Flow 'X' Example 3: This is the same as Example 1a, except the windows are on your Left Monitor. Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 1920 -DestinationScreenHeight 1080 -DestinationMonitor 'L' -Rows 4 -Cols 2 -Flow 'Y' Example 4: This is the same as Example 2, except the windows are on your Right Monitor. Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 1920 -DestinationScreenHeight 1080 -DestinationMonitor 'R' -Rows 3 -Cols 2 -OffsetLeft 0 -OffsetTop 0 -Flow 'X' Example 5: This is a nice hack if you have 2 screens. You want the windows to span two screens, rather than being confined to a single screen. Assumes your second screen is to the left of your Main Monitor. This will open 4 windows: 2 your Left Monitor, 2 on your Main monitor, arranged horizontally, each taking up 1/2 the width and 1/2 the height of each screen Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 3840 -DestinationScreenHeight 1080 -DestinationMonitor 'L' -Rows 2 -Cols 4 -OffsetLeft 1920 -OffsetTop 0 -Flow 'X' Example 6: This is a nice hack if you have 3 screens. You want the windows to span three screens, rather than being confined to a single screen. Assumes your second screen is to the left of your Main Monitor, and the third is to the right of your Main Monitor. This will open 6 windows: There will be on the first row, 2 windows your Left Monitor, 2 on your Main monitor, 2 on your Right Monitor, arranged horizontally, each taking up 1/3 the width and 1/3 the height of each screen Position-ExplorerWindow -paths @('D:\My Data Folder\Data1', 'D:\My Data Folder\Data2', 'D:\My Data Folder\Data3', 'D:\My Data Folder\Data\', '\\MYSERVER\public', '\\192.168.0.1\share') -DestinationScreenWidth 5760 -DestinationScreenHeight 1080 -DestinationMonitor 'L' -Rows 3 -Cols 3 -OffsetLeft 3840 -OffsetTop 0 -Flow 'X' .NOTES ################################################################################################################################ # Dependencies: # # - UIAutomation PS module: https://uiautomation.codeplex.com/wikipage?title=Getting%20a%20window&referringTitle=Documentation # ################################################################################################################################ #> [CmdletBinding()] Param( [Parameter(Mandatory=$False,Position=0)] [switch]$ModeEasy , [Parameter(Mandatory=$True,Position=1)] [String[]]$Paths , [Parameter(Mandatory=$False,Position=2)] #[ValidateRange(640, [int]::MaxValue)] [Int]$DestinationScreenWidth , [Parameter(Mandatory=$False,Position=3)] #[ValidateRange(360, [int]::MaxValue)] [Int]$DestinationScreenHeight , [Parameter(Mandatory=$False,Position=4)] [ValidateSet('M', 'L', 'R', 'T', 'B')] [String]$DestinationMonitor = 'M' , [Parameter(Mandatory=$False,Position=5)] #[ValidateRange(1, [int]::MaxValue)] [Int]$Rows = 4 , [Parameter(Mandatory=$False,Position=6)] #[ValidateRange(1, [int]::MaxValue)] [Int]$Cols = 2 , [Parameter(Mandatory=$False,Position=7)] #[ValidateRange(0, [int]::MaxValue)] [Int]$OffsetLeft = 0 , [Parameter(Mandatory=$False,Position=8)] #[ValidateRange(0, [int]::MaxValue)] [Int]$OffsetTop = 0 , [Parameter(Mandatory=$False,Position=9)] [ValidateSet('X', 'Y')] [String]$Flow = 'Y' , [Parameter(Mandatory=$False,Position=10)] #[ValidateRange(0, 1)] [Int]$DebugLevel = 0 ) begin { # NOTE: No longer using UIAutomation Module. # Import Dependency - UIAutomation Module #try { # Import-Module UIAutomation -ErrorAction Stop # [UIAutomation.Preferences]::HighlightParent = $False # [UIAutomation.Preferences]::Highlight = $False #}catch { # throw $_.Exception.Message #} # Error preference $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' if ($PSVersionTable.PSVersion.Major -gt 5) { Write-Host "Module is only supported on Powershell v5 or lower." return } # Get all main monitors resolution # Doesn't work for Windows 7 PSv2 #$mainMonitor = Get-Wmiobject Win32_Videocontroller #$mainMonitorWidth = $mainMonitor.CurrentHorizontalResolution #$mainMonitorHeight = $mainMonitor.CurrentVerticalResolution # Get all main monitors resolution # From: https://stackoverflow.com/questions/7967699/get-screen-resolution-using-wmi-powershell-in-windows-7/7968063#7968063 # The returned screen objects appears to be in order of the physical position of the monitors, from left to right, # regardless of what the monitor's ID in Control Panel's / Settings 'Identify' feature shows. Add-Type -AssemblyName System.Windows.Forms if ([System.AppDomain]::CurrentDomain.GetAssemblies() | ? { $_.FullName -match 'System.Windows.Forms' }) { $screens = [System.Windows.Forms.Screen]::AllScreens }else { throw "Failed to load assembly: System.Windows.Forms" } # Lets get the Main Monitor's resolution Write-Host "`n[Detecting Main Monitor Resolution]" -ForegroundColor Cyan $mainMonitor = $screens | Where-Object { $_.Primary } | Select-Object -First 1 if (!$mainMonitor) { Write-Warning "Unable to auto-detect main monitor's resolution." throw "Unable to auto-detect main monitor's resolution." } # Use working area instead of bounds #$mainMonitorWidth = $mainMonitor.Bounds.Width #$mainMonitorHeight = $mainMonitor.Bounds.Height $mainMonitorWidth = $mainMonitor.WorkingArea.Width $mainMonitorHeight = $mainMonitor.WorkingArea.Height Write-Host "Main Monitor Resolution: $mainMonitorWidth x $mainMonitorHeight" -ForegroundColor Green # If using simple mode, then consider the destination screen (i.e. pixel pool) to be the same as the main monitor's resolution if ($ModeEasy) { $DestinationScreenWidth = $mainMonitorWidth $DestinationScreenHeight = $mainMonitorHeight } # Determine number of monitors $numMonitors = $screens.Count # Doesn't work on Windows 7 PSv2 #$numMonitors = (Get-WmiObject WmiMonitorID -Namespace root\wmi).Count } process { try { Write-Host "[Position-ExplorerWindow options]" -ForegroundColor Cyan Write-Host "Paths: " -ForegroundColor Green $Paths | ForEach-Object { Write-Host " $($_.Trim())" -ForegroundColor Green } Write-Host "DestinationScreenWidth: $DestinationScreenWidth" -ForegroundColor Green Write-Host "DestinationScreenHeight: $DestinationScreenHeight" -ForegroundColor Green Write-Host "DestinationMonitor: $DestinationMonitor" -ForegroundColor Green Write-Host "Rows: $Rows" -ForegroundColor Green Write-Host "Cols: $Cols" -ForegroundColor Green Write-Host "ForegroundColor: $OffsetLeft" -ForegroundColor Green Write-Host "OffsetTop: $OffsetTop" -ForegroundColor Green Write-Host "Flow: $Flow" -ForegroundColor Green Write-Host "Debug: $DebugLevel" -ForegroundColor Green # Determine the Window Group Starting Position, each window's dimension Write-Host "`n[Calculating Window Group Starting Position, each window's dimension]" -ForegroundColor Cyan # Determine Window Group Starting Position - Get (x,y) coordinates, where the origin (0,0) is the Top-Left Corner of the Main Monitor if ($numMonitors -eq 1) { # Single-Monitor # Calculate left $left = 0 + $OffsetLeft # Calculate top $top = 0 + $OffsetTop }elseif ($numMonitors -gt 1) { # Multi-Monitor Switch ($DestinationMonitor) { 'M' { # Its just like Single-Monitor # Calculate left $left = 0 + $OffsetLeft # Calculate top $top = 0 + $OffsetTop } 'L' { # Calculate left $startingPoint = 0 - $DestinationScreenWidth $left = $startingPoint + $OffsetLeft # Calculate top $top = 0 + $OffsetTop } 'R' { # Calculate left $startingPoint = 0 + $mainMonitorWidth $left = $startingPoint + $OffsetLeft # Calculate top $top = 0 + $OffsetTop } 'T' { # Calculate left $left = 0 + $OffsetLeft # Calculate top $startingPoint = 0 - $DestinationScreenHeight $top = $startingPoint + $OffsetTop } 'B' { # Calculate left $left = 0 + $OffsetLeft # Calculate top $startingPoint = 0 + $DestinationScreenHeight $top = $startingPoint + $OffsetTop } } } # Ensure they are integers, or UIAutomation won't position them correctly $left = [math]::Floor($left) $top = [math]::Floor($top) # Determine each window's dimension $my_width = [math]::Floor( $DestinationScreenWidth / $Cols ) # e.g. 1920 / 2 $my_height = [math]::Floor( $DestinationScreenHeight / $Rows ) # e.g. 1080 / 4 Write-Host "NOTE: Origin (0, 0) is the Top-Left Corner of your Main Monitor." -ForegroundColor Green Write-Host "Starting Coordinates (left, top): ($left, $top)" -ForegroundColor Green Write-Host "Window Dimensions (width x height): $my_width x $my_height" -ForegroundColor Green # Flow Cursor $i = 0 # Path count $p = 0 Write-Host "`n[Opening Windows]" -ForegroundColor Cyan $my_left = $left $my_top = $top foreach ($Path in $Paths) { # Debug Write-Host "`nPath: $Path" -ForegroundColor Cyan $p++ Try { if (! (Test-Path -Path $Path -ErrorAction Stop)) { Write-Warning "Path does not exist: $Path. Skipping opening a window for this path." continue } }Catch { Write-Warning "Invalid path specified: $Path. Illegal charcters used in path. Skipping opening a window for this path." continue } if ( $p -gt ($Rows * $Cols) ) { Write-Warning "Number of windows exceeded rows*cols = $($Rows*$Cols). Increase the number of rows and columns." Write-Warning "Skipping opening a windows for path: $Path" continue } # Determine window position Switch ($Flow) { 'Y' { if ($DebugLevel -band 1) { Write-Host '`tFlow is Y. Calculating coordinates for this window...' } # Top-Down # If reached max number of rows: reset the cursor to Starting Position y coordinate, and get next left position if ($i -eq $Rows) { $i = 0 $my_left += $my_width } $my_top = $top + ($my_height * $i) } 'X' { if ($DebugLevel -band 1) { Write-Host '`tFlow is Y. Calculating coordinates for this window...' } # Left-to-Right # If reached max number of cols: reset the cursor to Starting Position x coordinate, and get next top position if ($i -eq $Cols) { $i = 0 $my_top += $my_height } $my_left = $left + ($my_width * $i) } } # Debug Write-Host "`tMy Coordinates (left, top): ($my_left, $my_top)" if ($DebugLevel -band 1) { Write-Host "`tMy Dimensions (width, height): $my_width x $my_height" } # We are going to use difference objects of explorer.exe ################# # Start-Process # ################# # Start-Process: https://ss64.com/ps/start-process.html # explorer.exe: https://ss64.com/nt/explorer.html # Note: A newly started explorer.exe subsequently spawns a child explorer.exe before killing itself. # Start a new explorer.exe process and get its pid Write-Host "`tStarting Explorer process..." -ForegroundColor Yellow $parent = Start-Process -FilePath explorer -ArgumentList "/separate,`"$Path`"" -PassThru $parent_pid = $parent.Id # Skip over this path if we didn't get a newly started explorer.exe if (!$parent_pid) { Write-Warning "Could not find parent explorer.exe. Skipping."; Continue } # Get the explorer processes before launching Write-Host "`tGetting Explorer processes..." -ForegroundColor Yellow $processes_prev = Get-Process explorer # Skip over this path if we didn't get any explorer instances. if (!$processes_prev) { Write-Warning "No explorer.exe instances found. Quitting."; Exit } if ($DebugLevel -band 1) { $processes_prev | Format-Table | Out-String | % { Write-Host $_.Trim() } } # Get the pid of the spawned child explorer.exe. This is achieved by getting a diff-object of explorer.exe processes until we find the spawned child's pid Write-Host "`tGetting spawned child process..." -ForegroundColor Yellow # Loop count $x = 0 $child_pid = $NULL while($child_pid -eq $NULL) { $SleepMilliseconds = 10 $x++ # Get explorer processes after starting the new explorer process if ($DebugLevel -band 1) { Write-Host "`t`tGetting Explorer processes..." -ForegroundColor Yellow } $processes_after = Get-Process explorer if ($DebugLevel -band 1) { $processes_after | Format-Table | Out-String | Write-Host } # Get the child process id from the difference object between two collections of explorer.exe # E.g. # Loop 0: $NULL # Loop 1: 7972 $child_pid = $(Compare-Object $processes_prev $processes_after -Property Id | Where-Object { $_.sideindicator -eq '=>'}).Id if ($DebugLevel -band 1) { Write-Host "`t`t >Diff: $child_pid" } # Successfully found a child process id. Print a message if ($child_pid) { if ($DebugLevel -band 1) { Write-Host "`tWe took $x loops to get the child process id: $child_pid" -ForegroundColor Green } Write-Host "`tWe found the child process" -ForegroundColor Green } # Stop looping if we can't find it if ($x -gt 100) { if ($DebugLevel -band 1) { Write-Host "We took too many loops($x) and $( $x * $SleepMilliseconds )ms and to find the child explorer process." -ForegroundColor Yellow } break } Start-Sleep -Milliseconds $SleepMilliseconds } if ($child_pid) { # Give some time before positioning and resizing window Start-Sleep -Milliseconds 100 # Try and reposition and resize Window Write-Host "`tRepositioning and Resizing window..." -ForegroundColor Green $success = Position-ResizeWindow -ProcessId $child_pid -Left $my_left -Top $my_top -Width $my_width -Height $my_height -DebugLevel $DebugLevel if ($success) { Write-Host "`tSuccessfully repositioned and resized window." -ForegroundColor Green # increment cursor $i++ }else { Write-Warning "Failed to reposition and resize window. The window is not movable or not resizable." } }else { Write-Warning "Could not find a child explorer.exe instance. Unable to position and resize a new Explorer window for path: $Path" } } # End paths loop }catch { Write-Error -ErrorRecord $_ -ErrorAction $CallerEA } } # End process # } |