PDQ Sticky notification - Systray and MSG box notificaitons for install complete for users to close
<#
.SYNOPSIS
Displays a notification using a tray balloon tip and/or a styled modal form.
.DESCRIPTION
Designed for PDQ Deploy using "Run As Logged On User". Can display tray and/or modal messages.
Supports non-blocking form display via -nowait, or blocking interaction with OK/Cancel/Timeout return codes.
.PARAMETER msg
Required. Message to display.
.PARAMETER title
Optional. Title of the modal or balloon (default: "Message").
.PARAMETER minutes
Optional. Tray icon visibility time in minutes (default: -1 = indefinite).
.PARAMETER timeout
Optional. Auto-close modal after X seconds. Returns 2 on timeout.
.PARAMETER NoSystray
Optional. Suppress tray balloon.
.PARAMETER NoForm
Optional. Suppress modal form.
.PARAMETER NoImage
Optional. Hide image from modal.
.PARAMETER Cancel
Optional. Show Cancel button. Returns -1 on cancel/[X].
.PARAMETER image
Optional. Override image path.
.PARAMETER nowait
Optional. Run modal in background process. No return value to caller.
.PARAMETER help
Optional. Show help message.
.EXAMPLES
Notify-Sticky.ps1 -msg "Complete" -timeout 10
Notify-Sticky.ps1 -msg "Done" -nowait
Notify-Sticky.ps1 -msg "Notice" -NoForm
#>
# Define parameters with default values
param (
[string]$msg, # Message to display (required)
[string]$title = "Message", # Modal/tray title (default: Message)
[int]$minutes = -1, # Tray lifetime in minutes (-1 = indefinite)
[int]$timeout = 0, # Modal auto-close timeout in seconds
[switch]$NoSystray, # Suppress tray balloon
[switch]$NoForm, # Suppress modal form
[switch]$NoImage, # Suppress image in modal
[switch]$Cancel, # Show Cancel button
[string]$image, # Override image path
[switch]$nowait, # Launch modal in background
[switch]$help # Show help text and exit
)
# Set default fallback image path
$DEFAULT_IMAGE_PATH = "\\gal-arc01\T\Logos-Icons\Galloway\Galloway Avatar Circle_CMYK.png"
# Show help content function
function Show-Help {
@"
Notify-Sticky.ps1 -msg "text" [options]
-title "text" Optional title (default: Message)
-minutes X Tray time (default: -1 = forever)
-timeout X Auto-close modal (in seconds) (returns 2)
-nowait Launch modal in background, don't wait
-NoSystray Skip tray balloon
-NoForm Skip modal window
-Cancel Show Cancel button (returns -1)
-NoImage Suppress image in modal
-image path.png Use alternate image path
-help Show help and exit
"@ | Write-Output
exit 1
}
# Handle help or missing message
if ($help -or !$PSBoundParameters.Count -or -not $msg) { Show-Help }
# Prevent empty output by suppressing both visual types
if ($NoForm -and $NoSystray) { Write-Error "Nothing to show: both -NoForm and -NoSystray set."; exit 1 }
# Load necessary Windows libraries
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Function to build and show custom modal dialog
function Show-CustomForm {
param (
[string]$title,
[string]$msg,
[string]$imgPath,
[switch]$NoImage,
[switch]$Cancel,
[int]$timeout
)
$timedOut = $false # Flag to indicate timeout
# Initialize modal window
$form = New-Object Windows.Forms.Form
$form.Text = $title
$form.Width = 600
$form.Height = 220
$form.StartPosition = "CenterScreen"
$form.TopMost = $true
$form.BackColor = [System.Drawing.Color]::WhiteSmoke
$form.FormBorderStyle = 'FixedDialog'
# Optional image rendering
if (-not $NoImage) {
$iconBox = New-Object Windows.Forms.PictureBox
$iconBox.SizeMode = 'Zoom'
$iconBox.Width = 48
$iconBox.Height = 48
$iconBox.Left = 10
$form.Controls.Add($iconBox)
}
# Message label control
$label = New-Object Windows.Forms.Label
$label.Text = $msg
$label.Font = New-Object Drawing.Font("Segoe UI", 12)
$label.AutoSize = $true
$label.MaximumSize = New-Object Drawing.Size(500, 0)
$label.Left = if (-not $NoImage) { 70 } else { 20 }
$form.Controls.Add($label)
# Position UI controls on form shown
$form.Add_Shown({
$label.Top = [Math]::Max(10, ($form.ClientSize.Height - $label.Height) / 2 - 20)
if (-not $NoImage -and $iconBox -ne $null) {
$iconBox.Top = [Math]::Max(10, ($form.ClientSize.Height - $iconBox.Height) / 2 - 20)
}
$buttonTop = $form.ClientSize.Height - 50
$okBtn.Top = $buttonTop
if ($Cancel) { $cancelBtn.Top = $buttonTop }
})
# OK button
$okBtn = New-Object Windows.Forms.Button
$okBtn.Text = "OK"
$okBtn.DialogResult = [System.Windows.Forms.DialogResult]::OK
$okBtn.Width = 90
$okBtn.Height = 30
$okBtn.Left = if ($Cancel) { 140 } else { ($form.ClientSize.Width - $okBtn.Width) / 2 }
$form.AcceptButton = $okBtn
$form.Controls.Add($okBtn)
# Optional Cancel button
if ($Cancel) {
$cancelBtn = New-Object Windows.Forms.Button
$cancelBtn.Text = "Cancel"
$cancelBtn.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$cancelBtn.Width = 90
$cancelBtn.Height = 30
$cancelBtn.Left = 270
$form.CancelButton = $cancelBtn
$form.Controls.Add($cancelBtn)
}
# Auto-close logic
if ($timeout -gt 0) {
$timer = New-Object Windows.Forms.Timer
$timer.Interval = $timeout * 1000
$timer.Add_Tick({
$timedOut = $true
$form.DialogResult = [Windows.Forms.DialogResult]::None
$form.Close()
})
$timer.Start()
}
# Attempt image loading
if (-not $NoImage -and $iconBox -ne $null -and (Test-Path $imgPath)) {
try {
$iconImage = [System.Drawing.Image]::FromFile($imgPath)
$iconBox.Image = $iconImage
} catch {}
}
$result = $form.ShowDialog()
return @{ Result = $result; TimedOut = $timedOut }
}
# Show system tray balloon if not suppressed
if (-not $NoSystray) {
$tray = New-Object System.Windows.Forms.NotifyIcon
$tray.Icon = [System.Drawing.SystemIcons]::Information
$tray.Text = $title
$tray.Visible = $true
$tray.ShowBalloonTip(5000, $title, $msg, [System.Windows.Forms.ToolTipIcon]::Info)
}
# Main modal logic
if (-not $NoForm) {
$imgPath = if ($image) { $image } else { $DEFAULT_IMAGE_PATH }
if ($nowait) {
# Prepare script block as background file
$escapedTitle = $title.Replace('"', '""')
$escapedMsg = $msg.Replace('"', '""')
$escapedImg = $imgPath.Replace('"', '""')
$scriptContent = @"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Show-CustomForm {
$(Get-Command Show-CustomForm | Select-Object -ExpandProperty ScriptBlock)
}
\$r = Show-CustomForm -title "$escapedTitle" -msg "$escapedMsg" -imgPath "$escapedImg" -NoImage:\$$($NoImage.IsPresent) -Cancel:\$$($Cancel.IsPresent) -timeout $timeout
if (\$r.TimedOut) { exit 2 }
elseif (\$r.Result -ne [Windows.Forms.DialogResult]::OK) { exit -1 }
else { exit 0 }
"@
$tempFile = [System.IO.Path]::GetTempFileName().Replace(".tmp", ".ps1")
[System.IO.File]::WriteAllText($tempFile, $scriptContent, [System.Text.Encoding]::Unicode)
# Launch form as background process
Start-Process powershell.exe -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-STA", "-File", "$tempFile"
exit 0
} else {
$r = Show-CustomForm -title $title -msg $msg -imgPath $imgPath -NoImage:$NoImage -Cancel:$Cancel -timeout $timeout
if ($r.TimedOut) { exit 2 }
elseif ($r.Result -ne [System.Windows.Forms.DialogResult]::OK) { exit -1 }
else { exit 0 }
}
}
# Tray-only wait logic
if ($NoForm -and -not $NoSystray) {
if ($minutes -eq -1) {
while ($true) { Start-Sleep -Seconds 1 } # Run indefinitely
} else {
Start-Sleep -Seconds ($minutes * 60) # Wait specified minutes
}
$tray.Visible = $false
$tray.Dispose()
exit 0
}
Comments
Post a Comment