3

I have the following PowerShell function which works well, but the window opens up in the background behind the PowerShell ISE.

# Shows folder browser dialog box and sets to variable
function Get-FolderName() {
    Add-Type -AssemblyName System.Windows.Forms
    $FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
        SelectedPath = 'C:\Temp\'
        ShowNewFolderButton = $false
        Description = "Select Staging Folder."
    }
    # If cancel is clicked the script will exit
    if ($FolderBrowser.ShowDialog() -eq "Cancel") {break}
    $FolderBrowser.SelectedPath
} #end function Get-FolderName

I can see there's a .TopMost property that can be used with the OpenFileDialog class but this doesn't appear to transfer over to the FolderBrowserDialog class.

Am I missing something?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
jshizzle
  • 467
  • 1
  • 11
  • 23

3 Answers3

4

Hope this helps

Add-Type -AssemblyName System.Windows.Forms
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowser.Description = 'Select the folder containing the data'
$result = $FolderBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true }))
if ($result -eq [Windows.Forms.DialogResult]::OK){
$FolderBrowser.SelectedPath
} else {
exit
}

//Edit to comment

There are 2 variants (overloads) of the ShowDialog () method.

See documentation: http://msdn.microsoft.com/en-us/library/system.windows.forms.openfiledialog.showdialog%28v=vs.110%29.aspx

In the second variant, you can specify the window that should be the mother of the dialogue.

Topmost should be used sparingly or not at all! If multiple windows are topmost then which is topmost? ;-)) First try to set your window as a mother then the OpenfileDialog / SaveFileDialog should always appear above your window:

$openFileDialog1.ShowDialog($form1)

If that's not enough, take Topmost.

Your dialogue window inherits the properties from the mother. If your mother window is topmost, then the dialog is also topmost.

Here is an example that sets the dialogue Topmost.

In this example, however, a new unbound window is used, so the dialog is unbound.

$openFileDialog1.ShowDialog((New - Object System.Windows.Forms.Form - Property @{TopMost = $true; TopLevel = $true}))
Jaapaap
  • 221
  • 1
  • 5
  • Ok, so I did find this method on my searches but it didn't always appear to work and just seemed to stop vscode from accepting key entry in the console for a credential call after. Was hoping to add "TopMost = $true" to my list of properties in the original code I provided but didn't doesn't like it and can't see why if it can be used in yours...? – jshizzle Jan 04 '19 at 10:49
  • @Jaapaap Unfortunately, your edit is all about the `OpenFileDialog`, not the [FolderBrowserDialog](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.folderbrowserdialog?view=netframework-4.7.2) – Theo Jan 04 '19 at 14:54
1

A reliable way of doing this is to add a piece of C# code to the function. With that code, you can get a Windows handle that implements the IWin32Window interface. Using that handle in the ShowDialog function will ensure the dialog is displayed on top.

Function Get-FolderName {   
    # To ensure the dialog window shows in the foreground, you need to get a Window Handle from the owner process.
    # This handle must implement System.Windows.Forms.IWin32Window
    # Create a wrapper class that implements IWin32Window.
    # The IWin32Window interface contains only a single property that must be implemented to expose the underlying handle.
    $code = @"
using System;
using System.Windows.Forms;

public class Win32Window : IWin32Window
{
    public Win32Window(IntPtr handle)
    {
        Handle = handle;
    }

    public IntPtr Handle { get; private set; }
}
"@

    if (-not ([System.Management.Automation.PSTypeName]'Win32Window').Type) {
        Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms.dll -Language CSharp
    }
    # Get the window handle from the current process
    # $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
    # Or write like this:
    $owner = [Win32Window]::new([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

    # Or use the the window handle from the desktop
    # $owner =  New-Object Win32Window -ArgumentList (Get-Process -Name explorer).MainWindowHandle
    # Or write like this:
    # $owner = [Win32Window]::new((Get-Process -Name explorer).MainWindowHandle)

    $FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
        SelectedPath = 'C:\Temp\'
        ShowNewFolderButton = $false
        Description = "Select Staging Folder."
    }
    # set the return value only if a selection was made
    $result = $null
    If ($FolderBrowser.ShowDialog($owner) -eq "OK") {
        $result = $FolderBrowser.SelectedPath
    }
    # clear the dialog from memory
    $FolderBrowser.Dispose()

    return $result
}

Get-FolderName

You can also opt for using the Shell.Application object with something like this:

# Show an Open Folder Dialog and return the directory selected by the user.
function Get-FolderName {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [string]$Message = "Select a directory.",

        [string]$InitialDirectory = [System.Environment+SpecialFolder]::MyComputer,

        [switch]$ShowNewFolderButton
    )

    $browserForFolderOptions = 0x00000041                                  # BIF_RETURNONLYFSDIRS -bor BIF_NEWDIALOGSTYLE
    if (!$ShowNewFolderButton) { $browserForFolderOptions += 0x00000200 }  # BIF_NONEWFOLDERBUTTON

    $browser = New-Object -ComObject Shell.Application
    # To make the dialog topmost, you need to supply the Window handle of the current process
    [intPtr]$handle = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle

    # see: https://msdn.microsoft.com/en-us/library/windows/desktop/bb773205(v=vs.85).aspx
    $folder = $browser.BrowseForFolder($handle, $Message, $browserForFolderOptions, $InitialDirectory)

    $result = $null
    if ($folder) { 
        $result = $folder.Self.Path 
    } 

    # Release and remove the used Com object from memory
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($browser) | Out-Null
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()


    return $result
}

$folder = Get-FolderName
if ($folder) { Write-Host "You selected the directory: $folder" }
else { "You did not select a directory." }
Theo
  • 57,719
  • 8
  • 24
  • 41
  • Hi Theo, many thanks for helping and providing this code. I've had to put this requirement at the back of the list due to deadlines but will revisit when I can. Apologies for the late reply but this is appreciated. – jshizzle Jan 07 '19 at 10:12
1

I just found an easy way to get PowerShell's IWin32Window value so forms can be modal. Create a System.Windows.Forms.NativeWindow object and assign PowerShell's handle to it.

function Show-FolderBrowser 
{       
    Param ( [Parameter(Mandatory=1)][string]$Title,
            [Parameter(Mandatory=0)][string]$DefaultPath = $(Split-Path $psISE.CurrentFile.FullPath),
            [Parameter(Mandatory=0)][switch]$ShowNewFolderButton)

    $DefaultPath = UNCPath2Mapped -path $DefaultPath; 

    $FolderBrowser = new-object System.Windows.Forms.folderbrowserdialog;
    $FolderBrowser.Description = $Title;
    $FolderBrowser.ShowNewFolderButton = $ShowNewFolderButton;
    $FolderBrowser.SelectedPath = $DefaultPath;
    $out = $null;

    $caller = [System.Windows.Forms.NativeWindow]::new()
    $caller.AssignHandle([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)

    if (($FolderBrowser.ShowDialog($caller)) -eq [System.Windows.Forms.DialogResult]::OK.value__)
    {
        $out = $FolderBrowser.SelectedPath;
    }

    #Cleanup Disposabe Objects
    Get-Variable -ErrorAction SilentlyContinue -Scope 0  | Where-Object {($_.Value -is [System.IDisposable]) -and ($_.Name -notmatch "PS\s*")} | ForEach-Object {$_.Value.Dispose(); $_ | Clear-Variable -ErrorAction SilentlyContinue -PassThru | Remove-Variable -ErrorAction SilentlyContinue -Force;}

    return $out;
}