A nicer PromptForChoice for the PowerShell Console Host

378322049_c01db2cbf5_m
Sometimes it’s not possible to fully automate a certain process and we need some input from the user(s) of the script in order to determine the further path of action. If this is based on a fixed set of choices the built-in PromptForChoice method can come to the rescue. Here is an example:


$Title = "Title"
$Info = "Pick Something!"
$options = echo Option1 Option2 Option3
$defaultchoice = 2
$selected = $host.UI.PromptForChoice($Title , $Info , $Options, $defaultchoice)
$options[$selected]

Running the code below in PowerShell ISE will produce the following result:

PromptForChoiceISE
Running the same from the PowerShell console though will not look as fancy:
PromptForChoiceConsole
The reason for the difference is that the underlying PromptForChoice method on the System.Management.Automation.Host.PSHostUserInterface is declared as an abstract method. This basically means that the implementation details are up to the respective PowerShell host (as long as the method complies with the declaration).
As a result your script will not provide a consistent user experience across PowerShell hosts (e.g. ISE, Console). Because of this I wrote a little Windows.Form based helper function that provides the same features as PromptForChoice but will look the same across all PowerShell hosts:


#An alternative to the built-in PromptForChoice providing a consistent UI across different hosts
function Get-Choice {
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,Position=0)]
$Title,
[Parameter(Mandatory=$true,Position=1)]
[String[]]
$Options,
[Parameter(Position=2)]
$DefaultChoice = -1
)
if ($DefaultChoice -ne -1 -and ($DefaultChoice -gt $Options.Count -or $DefaultChoice -lt 1)){
Write-Warning "DefaultChoice needs to be a value between 1 and $($Options.Count) or -1 (for none)"
exit
}
Add-Type AssemblyName System.Windows.Forms
Add-Type AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
$script:result = ""
$form = New-Object System.Windows.Forms.Form
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog
$form.BackColor = [Drawing.Color]::White
$form.TopMost = $True
$form.Text = $Title
$form.ControlBox = $False
$form.StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen
#calculate width required based on longest option text and form title
$minFormWidth = 100
$formHeight = 44
$minButtonWidth = 70
$buttonHeight = 23
$buttonY = 12
$spacing = 10
$buttonWidth = [Windows.Forms.TextRenderer]::MeasureText((($Options | sort Length)[-1]),$form.Font).Width + 1
$buttonWidth = [Math]::Max($minButtonWidth, $buttonWidth)
$formWidth = [Windows.Forms.TextRenderer]::MeasureText($Title,$form.Font).Width
$spaceWidth = ($options.Count+1) * $spacing
$formWidth = ($formWidth, $minFormWidth, ($buttonWidth * $Options.Count + $spaceWidth) | Measure-Object Maximum).Maximum
$form.ClientSize = New-Object System.Drawing.Size($formWidth,$formHeight)
$index = 0
#create the buttons dynamically based on the options
foreach ($option in $Options){
Set-Variable "button$index" Value (New-Object System.Windows.Forms.Button)
$temp = Get-Variable "button$index" ValueOnly
$temp.Size = New-Object System.Drawing.Size($buttonWidth,$buttonHeight)
$temp.UseVisualStyleBackColor = $True
$temp.Text = $option
$buttonX = ($index + 1) * $spacing + $index * $buttonWidth
$temp.Add_Click({
$script:result = $this.Text; $form.Close()
})
$temp.Location = New-Object System.Drawing.Point($buttonX,$buttonY)
$form.Controls.Add($temp)
$index++
}
$shownString = '$this.Activate();'
if ($DefaultChoice -ne -1){
$shownString += '(Get-Variable "button$($DefaultChoice-1)" -ValueOnly).Focus()'
}
$shownSB = [ScriptBlock]::Create($shownString)
$form.Add_Shown($shownSB)
[void]$form.ShowDialog()
$result
}

view raw

Get-Choice.ps1

hosted with ❤ by GitHub

Using Get-Choice like this:

Get-Choice "Pick Something!" (echo Option1 Option2 Option3) 2

Will look in both ISE and Console like that:

GetChoice
The most notable parts of the function are probably in the loop on lines 46-59. Where the buttons are created dynamically based on the options provided.:

foreach ($option in $Options){
        Set-Variable "button$index" -Value (New-Object System.Windows.Forms.Button)
        $temp = Get-Variable "button$index" -ValueOnly
        $temp.Size = New-Object System.Drawing.Size($buttonWidth,$buttonHeight)
        $temp.UseVisualStyleBackColor = $True
        $temp.Text = $option
        $buttonX = ($index + 1) * $spacing + $index * $buttonWidth
        $temp.Add_Click({ 
            $script:result = $this.Text; $form.Close() 
        })
        $temp.Location = New-Object System.Drawing.Point($buttonX,$buttonY)
        $form.Controls.Add($temp)
        $index++
}

Similar to the way it works in PromptForChoice preceding a character from within the option values with an ampersand (e.g. Option &1) will make the button accessible via ALT-key + the letter (e.g. ALT + 1).
The function can also be found in my GitHub repo.

shareThoughts


Photo Credit: zachstern via Compfight cc

6 thoughts on “A nicer PromptForChoice for the PowerShell Console Host

  1. This is a nice interface option however, I would like it to return numeric representations of the choices instead of the verbose text in the button (sometimes my buttons are rather long) and having to type in the entire string is a bit cumbersome. per your example above, it would be nice to define a return value of 1 2 or 3 based on Red, Blue or Green, instead of having to spell out the choices. (i.e. if ($returnvalue = ‘red’) [myfunc1] elseif ($returnvalue=’green’) {myfunc2})

    I also agree with a $text option to help explain the prompt. You can include both a prompt and text with PromptfforChoice:

    $decision = $Host.UI.PromptForChoice(
    “Perform Action?” , “What to you want to do with the found device? You can install and APP, Reboot the device or Leave it alone”,@(‘&Leave it alone’, ‘&Install App’ ,’&Reboot Device ‘), 0)

    Like

  2. this is nice, thanks! only thing i would improve would be to add a $prompt block of text below the title bar and above the buttons, to better explain what these choices are for, and to give a consistent look/feel in your script – script name in $title, question/text in $prompt, and buttons in $options. ie: $title = “User Creation Script v2.1” … $prompt = “Which OU will the new user be in?” … “$options = (“IT-Staff”,”Admin-Staff”,”Teachers”,”Students”)

    i’ll see if i can add $prompt to the function so that it’ll properly resize the dialog box to fit, but my powershell isn’t that great yet. still learning.

    Liked by 1 person

    1. Hi Robert,
      The function could use some help. Here you go.
      The Get-Choice function accepts three parameters:
      1. Title: Mandatory. Defines the title of the dialog window.
      2. Options: Mandatory. Defines the options a user can choose from, provided as an array of strings.
      3. DefaultChoice: Optional. If a number is provided for this parameter it defines the option that is the default choice when the dialog is started (defaults to -1 which means there is no default choice).

      An example. If you want to create a dialog that asks the user about a choice between three colors:
      $title = ‘Please pick a color’
      $options = ‘Red’, ‘Blue’, ‘Green’
      #make Blue the default choice
      $default = 2
      Get-Choice -Title $title -Options $options -DefaultChoice $default

      Kind regards,
      Dirk

      Like

      1. I’ve been trying to use this function in a script I’m working on… I understand that I can pick a specific button, but how do I get it to run a function based on my choice?

        Like

      2. The Get-Choice function returns the chosen option as a string. You could use IF constructs to call a function of your choice based on the return value. E.g. .
        $title = ‘Please pick a color’
        $options = ‘Red’, ‘Blue’, ‘Green’
        #make Blue the default choice
        $default = 2
        $returnValue = Get-Choice -Title $title -Options $options -DefaultChoice $default
        if ($returnValue -eq ‘Red’) { myfunc1 }
        elseif ($returnValue -eq ‘Green’) { myfunc2 }

        Like

I'd love to hear what you think

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s