0

I've found solutions to programmatically position windows. There's even my question (similar, not exact match). Nothing in the docs for Invoke-Item states anything about it. I don't like the advanced solutions due to the complexity (hopefully due to a general purpose and a, indeed, higher code standard). I'm looking for a lazy man's solution (possibly having same caveats and limitations).

I have a script that loops through an array of strings and attempts to open a window for each path found. I know the size of my screens. I don't need to ensure scalability nor interpolate to other systems. "Works on my machine" is perfectly sustainable in this case.

$BaseDir = "C:\Blaha"
$SubDirs = @("monkey", "donkey", "wonkey")

foreach($Dir in $SubDirs){
  $TargetDir = Join-Path $BaseDir $Dir
  if(Test-Path $TargetDir) { Invoke-Item($TargetDir) }
}

I would like to be able to specify a fixed position for each window opened by the Invoke-Item statement. At this stage, I'm happy if each such is opened at the same location, say at 150px from the top and 230px from the left.

Is that doable?

Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438
  • [`Start-Process` has options to do more than `Invoke-Item`](https://stackoverflow.com/questions/54812828/); Try `$x = Start-Process -FilePath $TargetDir -PassThru` Then apply [`Set-Window -Id ($x.Id)`](https://superuser.com/a/1324170/376602) ? – JosefZ May 07 '23 at 11:53
  • @JosefZ Two questions. One is that it seems I get the error *This command cannot be run completely because the system cannot find all the information required* when I try to use `-PassThru` **for directories** (files work fine). How do I approach it? Second one is that I don't have `Set-Window` by default. Did you mean the same approach as I link to in my question (the "big and proper" solution, instead of the "quick and dirty" one)? – Konrad Viltersten May 07 '23 at 14:45
  • Have a look here: https://gist.github.com/indented-automation/cbad4e0c7e059e0b16b4e42ba4be77a1 specifically at `Set-WindowPosition` – Santiago Squarzon May 07 '23 at 15:20
  • My bad, sorry. The `-FilePath` option requires name of an executable file or of a document… Then, use (in windows) e.g. ```if(Test-Path $TargetDir -PathType Container) { $aux = Start-Process -FilePath explorer.exe -ArgumentList $TargetDir -PassThru } elseif(Test-Path $TargetDir -PathType Leaf) { $aux = Start-Process -FilePath $TargetDir -PassThru } else { $aux = $null }``` – JosefZ May 07 '23 at 15:21
  • *I don't have `Set-Window` by default* - nobody guess, I'd say. You need to add it to the current session by running [dot-sourced](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.3#dot-sourcing-operator-) the script where the `Set-Window` function is defined… – JosefZ May 07 '23 at 15:27
  • @JosefZ Thanks for the clarification. The thing is that I don't target a file but a **directory**, literally opening a Windows Explorer window showing the files in it. It may be unexpected but, yes, I'm opening a number of windows on the screen to show what's in them. The point is to position those, not having to move them manually (as it's tedious and takes time). – Konrad Viltersten May 08 '23 at 06:00
  • In general, the application being launched gets to put its window where the application wants, not the caller that launched the application. For a shell command, opening an explorer window, I don't believe there are any APIs that allow you to position it as the launcher. – Garr Godfrey May 19 '23 at 16:27
  • @GarrGodfrey Why don't you put that as answer to be accepted? It's still a bounty worth response. And who knows, maybe your answer will trigger someone to prove you wrong... :) – Konrad Viltersten May 19 '23 at 19:56

2 Answers2

1

The answer to whether there is a "simple" way, is No, there isn't.

There are very complex ways to make it happen, and you can get them to work MOST of the time. The answer from JosefZ is an example of one of the complicated methods.

See, the application that creates the windows controls where they are positioned. These applications allow users to move windows around and resize them, and they also do a lot to automatically position based on the active screen, the screen size, where other windows are positioned and the history of where windows were used in the past.

So, unless the application provides a mechanism to give window position through a command line or config file, you are left with some complicated solutions. Generally, these involve:

  • running an application with elevated privileges (as Administrator). This is necessary because you are, in effect, interfering with other processes, that may be elevated.

  • Find the window to reposition. There are various ways to search for the window. You can do it by title, or compare a list of windows that existed before launching to those that exist after. You need to filter out hidden windows and non-top level windows. Through some effort, you can find the filename of the application that launched each window, and use that in your filter.

  • use a windows user API to reposition the window.

After all that, it MIGHT work. Or, the application will just move the window right back to where it was afterwards.

For explorer.exe, no new application is launched. Behaviors can vary and it might reuse an existing window.

Bottom line is that this is rarely worth the effort involved, and pushing back on this requirement would be the first thing you should try.

Garr Godfrey
  • 8,257
  • 2
  • 25
  • 23
  • I wholeheartedly agree with you. And my solution is to install PowerToys from Microsoft, set up the Fancy Zones and manually position the windows. It's tedious and annoying, **yet**, the least tedious and effort-demanding approach in our scenario. Rant: setting size, position, coloring etc. in terminal windows should really be much easier to enhance user experience. PowerShell is much better than CMD but not nearly the perfection I expect from MS. – Konrad Viltersten May 23 '23 at 05:28
0

The script (requires considerably improved function Set-Window, enclosed below) opens each subdir in a new explorer window, and moves every next window 40 pixels right and down (resized merely to make screenshot clear):

if ( -not (Get-Command -Name Set-Window -ErrorAction SilentlyContinue) ) {
    . D:\PShell\Downloaded\WindowManipulation\Set-Window.ps1
}
$BaseDir = "C:\Blaha"
$BaseDir = "D:\PShell\DataFiles\Blaha"

$SubDirs = @("monkey", "donkey", "wonkey")
$XYcnt = 0
$XYinc = 32
$auxArray = [System.Collections.ArrayList]::new()
foreach($Dir in $SubDirs) {
    $TargetDir = Join-Path -Path $BaseDir -ChildPath $Dir
    if (Test-Path $TargetDir -PathType Container) {
        Start-Process -FilePath "c:\Windows\explorer.exe" -ArgumentList "/root,$TargetDir"
        # Invoke-Item -Path $TargetDir
        Start-Sleep -Seconds 3
        $rex = "^$Dir$|^$([regex]::Escape($TargetDir))$"
        $aux = Get-Process -Name explorer |
            Where-Object MainWindowTitle -match $rex
        if ( $null -ne $aux ) {
            $XYpos = $XYcnt*$XYinc
            [void]$auxArray.Add( 
                $(Set-Window -Id $($aux.Id) -X $XYpos -Y $XYpos -Height 440 -Passthru)
            )
            $XYcnt++
        }
    } <# elseif (Test-Path $TargetDir -PathType Leaf) {
        $aux = Start-Process -FilePath $TargetDir -PassThru
        # Invoke-Item -Path $TargetDir
    } <##>
}

$auxArray| Format-Table -AutoSize

Result: D:\PShell\SO\76193105.ps1

  Id ProcessName Size     TopLeft BottomRight WindowTitle
  -- ----------- ----     ------- ----------- -----------
5152 explorer    1130,440 0,0     1130,440    monkey     
4180 explorer    1130,440 32,32   1162,472    donkey     
7016 explorer    1130,440 64,64   1194,504    wonkey     

Important: improved script Set-Window.ps1:

Function Set-Window {
<#
.SYNOPSIS
Retrieve/Set the window size and coordinates of a process window.

.DESCRIPTION
Retrieve/Set the size (height,width) and coordinates (x,y) 
of a process window.

.NOTES
Name:   Set-Window
Author: Boe Prox
Version History:
1.0//Boe Prox 11/24/2015 Initial build
1.1//JosefZ   19.05.2018 Treats more process instances 
                            of supplied process name properly
1.2//JosefZ   21.02.2019 Added parameter `Id`
1.3//JosefZ   07.05.2023 Type of input parameters changed:
                            [int]$Id to [int[]]$Id
                            [string]$ProcessName to [string[]]$ProcessName
                          Added parameter `Processes`
                          Added `MainWindowTitle` (noteproperty) to output 

The most recent version:
D:\PShell\Downloaded\WindowManipulation\Set-Window.ps1

.OUTPUTS
None
System.Management.Automation.PSCustomObject
System.Object

.EXAMPLE
Get-Process powershell | Set-Window -X 20 -Y 40 -Passthru -Verbose
VERBOSE: powershell (Id=11140, Handle=132410)

Id          : 11140
ProcessName : powershell
Size        : 1134,781
TopLeft     : 20,40
BottomRight : 1154,821

Description: Set the coordinates on the window for the process PowerShell.exe

.EXAMPLE
$windowArray = Set-Window -Passthru
WARNING: cmd (1096) is minimized! Coordinates will not be accurate.

    PS C:\>$windowArray | Format-Table -AutoSize

  Id ProcessName    Size     TopLeft       BottomRight  
  -- -----------    ----     -------       -----------  
1096 cmd            199,34   -32000,-32000 -31801,-31966
4088 explorer       1280,50  0,974         1280,1024    
6880 powershell     1280,974 0,0           1280,974     

Description: Get the coordinates of all visible windows and save them into the
             $windowArray variable. Then, display them in a table view.

.EXAMPLE
Set-Window -Id $PID -Passthru | Format-Table
​‌‍
  Id ProcessName Size     TopLeft BottomRight
  -- ----------- ----     ------- -----------
7840 pwsh        1024,638 0,0     1024,638

Description: Display the coordinates of the window for the current 
             PowerShell session in a table view.

#>
[cmdletbinding(DefaultParameterSetName='Name')]
Param (
    # Name of the process to determine the window characteristics. 
    # (All processes if omitted).
    [parameter(Mandatory=$False,ValueFromPipeline=$True,
                                ValueFromPipelineByPropertyName=$False,
                                ParameterSetName='Name')]
    [Alias("ProcessName")][string[]]$Name='*',

    # Id of the process to determine the window characteristics.
    [parameter(Mandatory=$True, ValueFromPipeline=$False,
                                ValueFromPipelineByPropertyName=$True,
                                ParameterSetName='Id')]
    [int[]]$Id,
    [parameter(Mandatory=$True, ValueFromPipeline=$True,
                                ValueFromPipelineByPropertyName=$False,
                                ParameterSetName='Process')]

    # Process to retrieve/determine the window characteristics.
    [System.Diagnostics.Process[]]$Processes,

    # Set the position of the window in pixels from the left.
    [int]$X,

    # Set the position of the window in pixels from the top.
    [int]$Y,

    # Set the width of the window.
    [int]$Width,

    # Set the height of the window.
    [int]$Height,

    # Returns the output object of the window.
    [switch]$Passthru
)
Begin {
    Try { 
        [void][Window]
    } Catch {
    Add-Type @"
        using System;
        using System.Runtime.InteropServices;
        public class Window {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetWindowRect(
            IntPtr hWnd, out RECT lpRect);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public extern static bool MoveWindow( 
            IntPtr handle, int x, int y, int width, int height, bool redraw);
              
        [DllImport("user32.dll")] 
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ShowWindow(
            IntPtr handle, int state);
        }
        public struct RECT
        {
        public int Left;        // x position of upper-left corner
        public int Top;         // y position of upper-left corner
        public int Right;       // x position of lower-right corner
        public int Bottom;      // y position of lower-right corner
        }
"@
    }
}
Process {
    $Rectangle = New-Object RECT
    if ($PSBoundParameters['Debug']) {
        $DebugPreference = [System.Management.Automation.ActionPreference]::Continue
    }
    If ( $PSBoundParameters.ContainsKey('Id') ) {
        $Processes = Get-Process -Id $Id -ErrorAction SilentlyContinue
        Write-Debug "Id"
    } elseIf ( $PSBoundParameters.ContainsKey('Name') ) {
        $Processes = Get-Process -Name $Name -ErrorAction SilentlyContinue
        Write-Debug "Name"
    } else {
        Write-Debug "Process"
    }
    if ( $null -eq $Processes ) {
        If ( $PSBoundParameters['Passthru'] ) {
            Write-Warning 'No process match criteria specified'
        }
    } else {
        $Processes | ForEach-Object {
            $Handle = $_.MainWindowHandle
            Write-Verbose "$($_.ProcessName) `(Id=$($_.Id), Handle=$Handle`)"
            if ( $Handle -eq [System.IntPtr]::Zero ) { return }
            $Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)
            If (-NOT $PSBoundParameters.ContainsKey('X')) {
                $X = $Rectangle.Left            
            }
            If (-NOT $PSBoundParameters.ContainsKey('Y')) {
                $Y = $Rectangle.Top
            }
            If (-NOT $PSBoundParameters.ContainsKey('Width')) {
                $Width = $Rectangle.Right - $Rectangle.Left
            }
            If (-NOT $PSBoundParameters.ContainsKey('Height')) {
                $Height = $Rectangle.Bottom - $Rectangle.Top
            }
            If ( $Return ) {
                $Return = [Window]::MoveWindow($Handle, $x, $y, $Width, $Height,$True)
            }
            If ( $Passthru.IsPresent ) {
                $Rectangle = New-Object RECT
                $Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)
                If ( $Return ) {
                    $Height      = $Rectangle.Bottom - $Rectangle.Top
                    $Width       = $Rectangle.Right  - $Rectangle.Left
                    $Size        = New-Object System.Management.Automation.Host.Size        -ArgumentList $Width, $Height
                    $TopLeft     = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Left , $Rectangle.Top
                    $BottomRight = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Right, $Rectangle.Bottom
                    If ($Rectangle.Top    -lt 0 -AND 
                        $Rectangle.Bottom -lt 0 -AND
                        $Rectangle.Left   -lt 0 -AND
                        $Rectangle.Right  -lt 0) {
                        Write-Warning "$($_.ProcessName) `($($_.Id)`) is minimized! Coordinates will not be accurate."
                    }
                    $Object = [PSCustomObject]@{
                        Id          = $_.Id
                        ProcessName = $_.ProcessName
                        Size        = $Size
                        TopLeft     = $TopLeft
                        BottomRight = $BottomRight
                        WindowTitle = $_.MainWindowTitle
                    }
                    $Object
                }
            }
        }
    }
}
}

Explanation

On the assumption that default application for a filesystem folder is Windows default explorer.exe, Invoke-Item does not suffice: if I run your script unchanged then all new open windows are choked down by program manager process (see also the first snapshot):

Get-Process -Name explorer
Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName          
-------  ------    -----      -----     ------     --  -- -----------          
   3275     168   175140     160808      20.14   3904   1 explorer

explorer with Invoke-Item

After running the provided script SO\76193105.ps1 (with Start-Process instead of Invoke-Item):

windows moved and resized

and we can see that each window is open in a stand-alone process:

Set-Window -Name explorer -Passthru | Format-Table -AutoSize
  Id ProcessName Size     TopLeft BottomRight WindowTitle
  -- ----------- ----     ------- ----------- -----------
3904 explorer    1280,50  0,974   1280,1024              
4180 explorer    1130,440 32,32   1162,472    donkey     
5152 explorer    1130,440 0,0     1130,440    monkey     
7016 explorer    1130,440 64,64   1194,504    wonkey

explorer with Start-Process

Note: The "^$Dir$|^$([regex]::Escape($TargetDir))$" regex covers both possible title bar values: leaf only or full path: enter image description here

JosefZ
  • 28,460
  • 5
  • 44
  • 83