Introducing the AnyBox

Edit: The example code has been updated to include functions present in v0.2.1, including the new function New-AnyBoxPrompt.

When considering making a script reusable for non-PS users, the biggest obstacle the you may encounter is the creation of a graphical user interface (GUI) to make the script presentable. If you want to avoid rolling up your sleeves to make a Powershell GUI, you may consider using the MessageBox or InputBox offered by the Windows Forms library. These solutions are great for extremely basic input/ouput, but may leave you desiring more. This is my motivation behind the AnyBox: to create a customizable user interface to facilitate displaying output and receiving input from users, making your Powershell scripts more user friendly. With simple adjustments to the parameters, you can create attractive GUIs to serve many different purposes.

To get started, install AnyBox from the Powershell Gallery or directly from PowerShell using the PowershellGet module and the following command:

Install-Module -Name 'AnyBox' -Repository PSGallery

Throughout this post, I will be giving examples to illustrate the purpose of the various parameters, but keep in mind that nearly all of the various options can be combined to create a GUI for your specific need.

Message

Let’s start with the simplest example; the standard message box:

Show-AnyBox -Title 'AnyBox Demo' -Message 'Hello world' -Buttons 'Hi' -MinWidth 300
simple

simple

The -Message parameter accepts an array of strings to print, each separated by a new line.

Show-AnyBox -Icon 'Question' -Title 'AnyBox Demo' -Message 'Hello world', 'Are you ready?' -Buttons 'No', 'Yes' -MinWidth 300

Similar to -Message is -Comment, which will include a text block in italics near the bottom of the AnyBox.

Show-AnyBox -Message 'Provide your name:' -Prompt @{} -Comment 'First name only' -Buttons 'OK'

Buttons

Any number of buttons can be added using the -Buttons parameter like so:

Show-AnyBox -Message 'Select one:' -Buttons 'This', 'That', 'Other'

The result returned from an AnyBox is a hashtable that contains what input was received. For buttons, the name of the key in the hashtable is the name of the button; the value indicates whether or not that button was selected.

Name    Value
----    -----
That    True
This    False
Other   False

The button layout can also be altered using the -ButtonRows parameter.

Show-AnyBox -Title 'AnyBox Demo' -Message 'Select a number:' -Buttons @(1..9) -ButtonRows 3

As you will soon see, user input can be validated within the AnyBox, and the user will be unable to proceed until valid input is entered. The -CancelButton parameter accepts a single button name to designate as the cancel button. The cancel button closes the window without validating input. It is also selected if the user presses the ‘ESC’ key on the keyboard.

Similarly, the single button name provided to -DefaultButton indicates the button that will serve as the default button. The default button is selected if the user presses the ‘Enter’ key on the keyboard.

Show-AnyBox -Message 'Enter anything:' -Prompt (New-AnyBoxPrompt -ValidateNotEmpty)
  -Buttons 'Cancel', 'Submit' -CancelButton 'Cancel' -DefaultButton 'Submit'

Lastly, -ShowCopyButton will include a special button that will copy the provided message to the clipboard.

Prompt

This is where things get interesting, but a bit more tricky; I hope some examples will simplify things.

Any number of prompts can be provided, and each prompt can be customized using its own set of options. A hashtable specifying the various properties will be cast to the custom type AnyBox.Prompt, defined as follows:

namespace AnyBox {
  public enum InputType {
    None, Text, FileOpen, FileSave, Checkbox, Password, Date, Link
  };

  public enum MessagePosition { Top, Left };

  public class Prompt
  {
    public string Name;
    public InputType InputType = InputType.Text;
    public string Message;
    public MessagePosition MessagePosition = MessagePosition.Top;
    public string DefaultValue;
    public System.UInt16 LineHeight = 1;
    public bool ReadOnly = false;
    public string[] ValidateSet;
    public bool ValidateNotEmpty = false;
    public System.Management.Automation.ScriptBlock ValidateScript;
  }
}

As seen in the definition, the following types of AnyBox.InputTypes are allowed:

  • Text (default)
  • FileOpen
  • FileSave
  • Checkbox
  • Password
  • Date
  • Link

The options provided to each prompt type include:

  • Name
  • InputType
  • Message
  • MessagePosition
  • DefaultValue
  • LineHeight
  • ReadOnly
  • ValidateNotEmpty
  • ValidateSet
  • ValidateScript

Similar to buttons, the hashtable returned by the AnyBox will contain key-value pairs for each prompt and the user input. However, unlike buttons, the name of the key in the hashtable is Input_#, where # is the index of the prompt, starting from zero.

EDIT: Based on user feedback, -Name and -MessagePosition were added to v0.2.1. -Name is used to identify the prompt in the resulting output, which overrides the default name of Input_#. The -MessagePosition can be either ‘Top’ or ‘Left’, and defaults to ‘Top’.

InputType.Text

Text is the default input type, so the simplest example is:

Show-AnyBox -Prompt (New-AnyBoxPrompt) -Buttons 'Submit'
Name        Value
----        -----
Input_0     Hello world
Submit      True

Here is an example use a DefaultValue and the LineHeight property set to 5

[string]$default_qry = @"
SELECT
  Name, COUNT(*) [Total]
FROM Table
WHERE Value < 10
GROUP BY Name
ORDER BY Timestamp
"@

[AnyBox.Prompt]$prompt = @{
  Message = 'Enter your query:'
  DefaultValue = $default_qry
  LineHeight = 5
  ValidateNotEmpty = $true
}

Show-AnyBox -Prompt $prompt -Buttons 'Cancel', 'Execute' -CancelButton 'Cancel' -ContentAlignment 'Left' -MinWidth 300

Validate

Getting user input is not the only struggle. Often, a bigger struggle is validating the user input we get. AnyBox aims to make that simpler by use of any of three prompt options:

  • ValidateNotEmpty
  • ValidateSet
  • ValidateScript

-ValidateNotEmpty is the simplest; the AnyBox will not proceed until some input is entered (or the specified cancel button is selected).

-ValidateSet has some interesting behavior; it will replace the text box with a combo box to ensure whatever the user selects is in the given set.

$prompt = New-AnyBoxPrompt -Message 'What is your favorite sport?' `
  -ValidateSet @('Basketball', 'Football', 'Baseball', 'Soccer', 'Hockey', 'Other')

Show-AnyBox -Icon 'Question' -Prompt $prompt -Buttons 'OK'
Name        Value
----        -----
Input_0     Basketball
OK      True

With -ValidateScript, the options are endless. If the script block you provide returns $true, the user input is considered valid.

$prompt = New-AnyBoxPrompt -Message 'Enter any number between 0 and 100:' `
  -ValidateScript { $_ -ge 0 -and $_ -le 100 }

Show-AnyBox -Prompt $prompt -Buttons 'Submit'

InputType.File[Open|Save]

The input types FileOpen and FileSave are similar to Text, with the addition of a button to the left of the textbox that opens either a OpenFileDialog or SaveFileDialog window, respectively.

Show-AnyBox -MinWidth 350 -Buttons 'Cancel', 'Submit' -Prompt @(
  (New-AnyBoxPrompt -InputType 'FileOpen' -Message 'Open File:', ReadOnly=$true),
  (New-AnyBoxPrompt -InputType 'FileSave' -Message 'Save File:', ReadOnly=$true)
)

InputType.Password

When the Password input type is specified, the user is presented a PasswordBox instead of a TextBox, so the password is masked and returned as a security string.

Show-AnyBox -Buttons 'Cancel', 'Login' -Prompt @(
  (New-AnyBoxPrompt -InputType 'Text' -Message 'User Name:' -ValidateNotEmpty),
  (New-AnyBoxPrompt -InputType 'Password' -Message 'Password:' -ValidateNotEmpty)
)
Name    Value
----    -----
Cancel  False
Input_0 donald
Input_1 System.Security.SecureString
Login   True

InputType.CheckBox

The Checkbox input is straightforward:

Show-AnyBox -Icon 'Question' -Buttons 'Cancel', 'Ignore' `
  -Message 'An error occurred. (Code=123)' `
  -Prompt (New-AnyBoxPrompt -InputType 'Checkbox' -Message "Don't ask again." -DefaultValue=$true)

InputType.Date

The Date input is also fairly straightforward:

$splat = @{
  InputType = 'Date'
  Message = 'Show all events after:'
  DefaultValue = (Get-Date).AddDays(-7)
  ValidateNotEmpty = $true
}

Show-AnyBox -Buttons 'OK' -Prompt (New-AnyBoxPrompt @splat)

Prompt Example

Prompts can be built dynamically. Here is a quick example:

$prompts = 1..7 | foreach {
  $type = [AnyBox.InputType]$_
  New-AnyBoxPrompt -InputType $type -Name "$type`_input" -Message "$type Input:"
}

$buttons = 1..6 | foreach {
  "Button $_"
}

$answer = Show-AnyBox -Prompt $prompts -Buttons $buttons -ButtonRows 2 -MinWidth 400
PS C:\> $answer

Name                           Value
----                           -----
Button 6                       False
Button 4                       True
FileOpen_input                 C:\Temp\Logs\old.log
Password_input                 System.Security.SecureString
Button 5                       False
Text_input                     hello world
Checkbox_input                 True
Date_input                     3/12/2018
Button 2                       False
Link_input                     False
Button 3                       False
Button 1                       False
FileSave_input                 C:\Temp\Logs\new.log
PS C:\> $prompts | select Name, Message, @{Name='UserInput';Expression={ $answer[$_.Name] }}

Name           Message         UserInput
----           -------         ---------
Text_input     Text Input:     hello world
FileOpen_input FileOpen Input: C:\Temp\Logs\old.log
FileSave_input FileSave Input: C:\Temp\Logs\new.log
Checkbox_input Checkbox Input: True
Password_input Password Input: System.Security.SecureString
Date_input     Date Input:     3/12/2018
Link_input     Link Input:     False

Data Grid

The AnyBox also has the ability to display data to a user in a DataGrid. All you need to do is pass an array to the -GridData parameter.

Show-AnyBox -Title 'Powershell Processes' -GridData @(
  Get-Process -Name 'powershell' |
  select Id, Name, TotalProcessorTime, Path
)

You may notice a few useful additions that are included automatically. Above the data grid is a message indicating how many items are in the grid, and a text box that allows users to filter the grid items.

Beneath the grid are two unique buttons, ‘Explore’ and ‘Save’. The ‘Explore’ button will open the data grid items in the default Powershell grid view using Out-GridView where more sophisticated filtering can be done. The ‘Save’ button will prompt the user for a path to save the grid items to a CSV file.

Both of these automatic options can be disabled using the -HideGridSearch parameter.

The parameter -SelectionMode is available for the data grid and controls how grid cells are selected. Selected grid items are made available in the AnyBox output via the ‘grid_select’ key.

Show-AnyBox -Title 'Select processes to kill' `
-HideGridSearch -SelectionMode 'MultiRow' -Buttons 'Cancel', 'Kill' `
-GridData @(Get-Process -Name '*note*' | select Id, Name, TotalProcessorTime, Path)
Name             Value
----            -----
Cancel          False
grid_select     {@{Id=2680; Name=notepad;...
Kill            True

Sometimes, you may find that you only have one object with many properties to display. By default, the AnyBox will display this object with one row and many columns. It may be more appropriate to melt the object to a long format. The AnyBox function includes a parameter, -GridAsList, that makes this simple.

Show-AnyBox -Title 'Wide (as-is)' -Buttons 'OK' -HideGridSearch -GridData $car
Show-AnyBox -Buttons 'OK' -HideGridSearch -GridData $car -GridAsList

Personalization

Image & Colors

Brand your AnyBox with an image. The -Image parameter accepts either:

  1. The path to an accessible image file (.png, .jpg, etc.)
  2. The base64 encoded string representing an image.

Additionally, -FontFamily, -FontColor, and -BackgroundColor allow you to

[hashtable]$font = @{ FontFamily = 'Courier New'; FontColor = 'CornflowerBlue'; FontSize=20 }

Show-AnyBox @font -WindowStyle 'None' -Message 'Hello World', 'Are you ready?' -Buttons 'Yes' `
  -BackgroundColor 'Black' -Image '.\banner.png'

By using a base64 string representing the image, you can save and reuse the string without worrying about whether or not the image file is accessible. For convenience, the AnyBox module includes a function ConvertTo-Base64 which accepts the path to an image file as input, and returns the base64 representation of it.

Window Style

Since AnyBox uses the default Windows modal, all of the System.Window.WindowStyle options are available:

  • None
  • Single Border Window
  • 3D Border Window

See: https://stackoverflow.com/a/7482728

  • Tool Window

Similarly, all System.Windows.ResizeMode options are available (detailed here). However, because the AnyBox works by using a StackPanel to stack the controls atop one another, no vertical resizing is possible; only horizontal resizing. For this reason, it may be best to stick with NoResize or CanMinimize, since maximizing the window will look odd. Also, consider using the ToolWindow style, which omits all but the ‘exit’ button.

EDIT: the above no longer applies since v0.2; a window can now be fully maximized.

A few notes on -ResizeMode… Maximizing and Minimizing is considered “resizing”. Thus:

  • “NoResize” will hide the minimize and maximize buttons, and will not allow users to resize the window.
  • “CanMinimize” will show the minimize button, but disable the maximize button and will not allow users to resize the window.
  • “CanResize” and “CanResizeWithGrip” will show the minimize and maximize buttons, and allow users to resize the window.

Timeout

Lastly, the AnyBox has a timeout feature that will close the window after the specified number of seconds. It is configured using the -Timeout and, when specified, the AnyBox output will include a key named TimedOut to indicate if the timeout was reached.

The switch parameter -Countdown will show a countdown in the AnyBox.

Name         Value
----          -----
I feel fine   False
TimedOut      True

Comprehensive Example

I threw together this example program to help you get the gist of the AnyBox. The script “Process Killer” will prompt you for a computer name and filter. It will return all matching processes, giving you the option to kill selected processes. It will then confirm and kill the selected processes, and loop back to the start.

Import-Module AnyBox

[string]$default_input = 'localhost'
[hashtable]$answer = $null
[bool]$continue = $true

[hashtable]$common = @{WindowStyle = 'ToolWindow'; Title = 'Process Killer'; CancelButton = 'Cancel'}

while ($continue) {

  $answer = Show-AnyBox @common -Buttons 'Cancel', 'Search' -DefaultButton 'Search' -Prompt @(
    (New-AnyBoxPrompt -Name 'pcName' -Message 'Computer Name:' -DefaultValue $default_input -ValidateScript { Test-Connection $_ -Count 1 -Quiet -ea 0 }),
    (New-AnyBoxPrompt -Name 'filter' -Message 'Process Name Filter:' -DefaultValue '*' -ValidateNotEmpty)
  )

  $continue = $answer['Search']

  if ($continue)
  {
    $computer_name = $answer['pcName']
    $default_input = $computer_name
    [string]$filter = $answer['filter'].Replace('*', '%')

    [array]$processes = $null
    [string]$msg = $null

    try {
      $processes = @(Get-WmiObject -cn $computer_name -Class Win32_Process -Filter "Name LIKE '$($filter)'" -ea Stop)

      if ($processes.Length -eq 0) {
        $msg = 'No processes match the given filter.'
      }
    }
    catch {
      $msg = $_.Exception.Message
    }

    if ($msg) {
      $answer = Show-AnyBox @common -Message $("Error: '{0}'" -f $msg) -Buttons 'Cancel', 'Retry'
      $continue = $answer['Retry']
    }
    else {
      $answer = Show-AnyBox @common -Buttons 'Cancel', 'Kill' -SelectionMode MultiRow `
                -GridData @($processes | select ProcessId, ProcessName, CommandLine)

      $continue = $answer['Kill']

      if ($continue)
      {
        [array]$toKill = @($answer['grid_select'] | select ProcessId, ProcessName)

        $answer = Show-AnyBox @common -Message 'Are you sure you want to', 'kill the following processes?' -HideGridSearch `
                  -GridData $toKill -Buttons 'Cancel', 'Confirm'

        $continue = $answer['Confirm']

        if ($continue)
        {
          [uint32[]]$killIDs = $toKill | select -ExpandProperty ProcessId

          $results = $processes | where { $killIDs -contains $_.ProcessId } | foreach {
            [int]$code = 0
            [string]$msg = $null

            try {
              $code = $_.Terminate().ReturnValue
              if ($code -eq 0) {
                $msg = 'Successfully closed.'
              }
            }
            catch {
              $msg = $_.Exception.Message
            }

            $_ | select ProcessId, ProcessName, @{Name='Code';Expression={$code}}, @{Name='Message';Expression={$msg}}
          }

          $answer = Show-AnyBox @common -Buttons 'Cancel', 'Run Again' -HideGridSearch -GridData $results

          $continue = $answer['Run Again']
        }
      }
    }
  }
}

Conclusion

Congrats, you made it to the end! I hope you find the AnyBox module a useful addition to your scripts. Please leave any questions or feedback in the comments below. If you encounter any issues, or want to dive into the code, check out the AnyBox repo on Github.

Related

Next
Previous
comments powered by Disqus