Ultimate PowerShell GUI Message Box Popup (Updated)

Standard message box popups using Wscript.Shell look boring, ugly, a bit 1990s and aren’t very flexible.  So we’ve created our own PowerShell GUI message box popup.

**UPDATED** – Now supports multiline messages that are too long for the default text area, and automatically adds a scrollbar if required. 

29/02/24 – Now supports positions

Wscript.Shell Popup using PowerShell

Take this example:

$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("This is a boring popup",3,"Boring popup")

This code produces this, which is a standard and boring looking windows form which closes after 3 seconds.

It also stalls any further code from running – in other words, it isn’t asynchronous.  I used to use it to say something like “Processing background tasks…” and show it for 5 seconds, when in actual it wasn’t processing anything at that time because it was stuck showing the popup message!

Alkane Popup PowerShell Message Box

So we wrote the Alkane popup instead.  It looks better (we actually borrowed the colour scheme from our favourite web notification popup called Noty), supports timeouts and also supports asynchronous messages!  There are message boxes for “success”, “error”, “information”, “warning” and of course “default”.  Here is an example success message box:

Modern Popup

And here is the code.  We include demonstrations of how to call the function synchronously and asynchronously, as well as with and without a timeout.

#usage
#New-Popup [message] [type] [position] [duration] [asynchronous]
#New-Popup "This is a message." "success" 0 $false
#[message] a string of text
#[type] options are "success" "warning" "error" "information".  A blank string will be default black text on a white background.
#[position] options are "topLeft" "topRight" "topCenter" "center" "centerLeft" "centerRight" "bottomLeft" "bottomCenter" "bottomRight"
#[duration] 0 will keep the popup open until clicked.  Any other integer will close after that period in seconds.
#[asynchronous] $true or $false.  $true will pop the message up and continue script execution (asynchronous).  $false will pop the message up and wait for it to timeout or be manually closed on click.

 

function Alkane-Popup() {

param(
[string]$message,
[string]$type,
[string]$position,
[int]$duration,
[bool]$async   
)

    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.Open()
    $PowerShell = [PowerShell]::Create().AddScript({
        param ($message, $type, $position, $duration)
        Add-Type -AssemblyName System.Windows.Forms    
    
        $Timer = New-Object System.Windows.Forms.Timer
        $Timer.Interval = 1000
        $back = "#FFFFFF"
        $fore = "#000000"
    
        switch ($type) {
            "success"  { $back = "#60A917"; $fore = "#FFFFFF"; break; }
            "warning"   { $back = "#FA6800"; $fore = "#FFFFFF"; break; }
            "information" { $back = "#1BA1E2"; $fore = "#FFFFFF"; break; }
            "error"  { $back = "#CE352C"; $fore = "#FFFFFF"; break; }
        }
    
        #Build Form
        $objForm = New-Object System.Windows.Forms.Form
        $objForm.ShowInTaskbar = $false
        $objForm.TopMost = $true
        $objForm.ForeColor =  [System.Drawing.ColorTranslator]::FromHtml($fore);
        $objForm.BackColor =  [System.Drawing.ColorTranslator]::FromHtml($back);
        $objForm.ControlBox = $false
        $objForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle    
        $objForm.Size = New-Object System.Drawing.Size(400,200)
        $marginx = 30
        $marginy = 30
        $tbWidth = ($objForm.Width) - ($marginx*2)
        $tbHeight = ($objForm.Height) - ($marginy*2)
        #Add Rich text box
        $objTB = New-Object System.Windows.Forms.RichTextBox
        $objTB.Location = New-Object System.Drawing.Size($marginx,$marginy) 

        #get primary screen width/height
        $monitor = [System.Windows.Forms.Screen]::PrimaryScreen
        $monitorWidth = $monitor.WorkingArea.Width
        $monitorHeight = $monitor.WorkingArea.Height
        $objForm.StartPosition = "Manual"

        #default center
        $objForm.Location = New-Object System.Drawing.Point((($monitorWidth/2) - ($objForm.Width/2)), (($monitorHeight/2) - ($objForm.Height/2)));

        switch ($position) {
            "topLeft"  { $objForm.Location = New-Object System.Drawing.Point(0,0); break; }
            "topRight"  { $objForm.Location =  New-Object System.Drawing.Point(($monitorWidth - $objForm.Width),0); break; }
            "topCenter"  { $objForm.Location = New-Object System.Drawing.Point((($monitorWidth/2) - ($objForm.Width/2)), 0); break; }
            "center"  { $objForm.Location = New-Object System.Drawing.Point((($monitorWidth/2) - ($objForm.Width/2)), (($monitorHeight/2) - ($objForm.Height/2))); break; }
            "centerLeft"  { $objForm.Location = New-Object System.Drawing.Point(0, (($monitorHeight/2) - ($objForm.Height/2))); break; }
            "centerRight"  { $objForm.Location = New-Object System.Drawing.Point(($monitorWidth - $objForm.Width), (($monitorHeight/2) - ($objForm.Height/2))); break; }
            "bottomLeft"  { $objForm.Location = New-Object System.Drawing.Point(0, ($monitorHeight - $objForm.Height)); break; }
            "bottomCenter"  { $objForm.Location = New-Object System.Drawing.Point((($monitorWidth/2) - ($objForm.Width/2)), ($monitorHeight - $objForm.Height)); break; }
            "bottomRight"  { $objForm.Location = New-Object System.Drawing.Point(($monitorWidth - $objForm.Width), ($monitorHeight - $objForm.Height)); break; }
        }

        $closeLink = New-Object System.Windows.Forms.LinkLabel
        $closeLink.Location = New-Object System.Drawing.Point(($objForm.Width-20),5)
        $closeLink.LinkColor = [System.Drawing.ColorTranslator]::FromHtml($fore);
        $closeLink.ActiveLinkColor = [System.Drawing.ColorTranslator]::FromHtml($fore);
        $closeLink.LinkBehavior = [System.Windows.Forms.LinkBehavior]::NeverUnderline;
        $closeLink.Font = "Arial,12px,style=Bold"   
        $closeLink.Text = "X"
        $closeLink.add_Click({
            $Timer.Dispose()
            $objForm.Dispose()   
        })
        $objForm.Controls.Add($closeLink)

        $objTB.Size = New-Object System.Drawing.Size($tbWidth,$tbHeight)
        $objTB.Font = "Arial,14px,style=Regular"      
        $objTB.Text = $message        
        $objTB.ForeColor =  [System.Drawing.ColorTranslator]::FromHtml($fore);
        $objTB.BackColor =  [System.Drawing.ColorTranslator]::FromHtml($back);
        $objTB.BorderStyle = 'None'        
        $objTB.DetectUrls = $false
        $objTB.SelectAll()
        $objTB.SelectionAlignment = 'Center'        
        $objForm.Controls.Add($objTB)   
        #deselect text after centralising it
        $objTB.Select(0, 0)
    
        #add some padding near scrollbar if visible
        $scrollCalc =  ($objTB.Width - $objTB.ClientSize.Width) #if 0 no scrollbar
        if ($scrollCalc -ne 0) {
            $objTB.RightMargin = ($objTB.Width-35)
        }

        #close form on click of textbox        
        $objTB.Add_Click({
            $Timer.Dispose()
            $objForm.Dispose()            
        })

        #close form on click of form
        $objForm.Add_Click({
        $Timer.Dispose()
            $objForm.Dispose()            
        })
        $script:countdown = $duration
    
        $Timer.Add_Tick({   
            --$script:countdown
            if ($script:countdown -lt 0)
            {           
                $Timer.Dispose();        
                $objForm.Dispose();                
            }
        })

        if ($duration -gt 0) {
            $Timer.Start()
        }

        #bring form to front when shown
        $objForm.Add_Shown({ 
            $this.focus() 
            $this.Activate();
            $this.BringToFront();
        })
        $objForm.ShowDialog() | Out-Null

    }).AddArgument($message).AddArgument($type).AddArgument($position).AddArgument($duration)

    $PowerShell.Runspace = $runspace    

    $state = @{
        Instance = $PowerShell
        Handle   = if ($async) { $PowerShell.BeginInvoke() } else {  $PowerShell.Invoke() }
    }

    $null = Register-ObjectEvent -InputObject $state.Instance -MessageData $state.Handle -EventName InvocationStateChanged -Action { 
        param([System.Management.Automation.PowerShell] $ps)
        if($ps.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') {             
            $ps.Runspace.Close()
            $ps.Runspace.Dispose()
            $ps.EndInvoke($Event.MessageData)
            $ps.Dispose()                 
            [GC]::Collect()
        }       
    }

}


Alkane-Popup "This is a default message displayed center.  I will not close until you click me and will wait before continuing script execution.`r`n`r`nI am also an example of a`r`nmultiline`r`nmessage`r`nthat`r`nis`r`ntoo`r`nlong`r`nfor`r`nthe`r`ndefault`r`ntextbox." "information" "center" 0 $false
write-host "Do some logic..."
write-host "Do some logic..."
Alkane-Popup "This is an error message displayed topLeft.  I will close after 9 seconds or if clicked, and will wait for this message to close before continuing script execution." "error" "topLeft" 9 $false
write-host "Do some more logic..."
Alkane-Popup "This is a warning message displayed bottomRight.  I will close after 7 seconds or if clicked and will not wait for this message to close before continuing script execution." "warning" "bottomRight" 7 $true
write-host "And some more..."
Alkane-Popup "This is an information message displayed centerLeft.  I will close after 5 seconds or if clicked and will not wait for this message to close before continuing script execution." "information" "centerLeft" 5 $true
write-host "And a bit more...."
Alkane-Popup "This is a success message displayed topRight.  I will close after 3 seconds or if clicked and will wait for this message to close before continuing script execution." "success" "topRight" 3 $false
write-host "Do the last bit."