<
.SYNOPSIS
Create a read-only WPF window containing a text block showing a message, optionally including parameters passed, with or without a title.
.DETAILS
To use this script in an automated action, take a copy of it, add the _clientMetricX parameters as required and set the message text in the param() block with {0}, {1}, etc as required and set any other parameters where you don't want the default
.PARAMETER _clientMetric1
Parameter passed from ControlUp record properties and replaced in message string. Where more than one is specified, the trailing digits are sorted numerically to determine order of replacement in the message string, eg {1} would be replaced by _clientMetric2
.PARAMETER message
The message to display in the dialogue. If specifying variables with _ prefix, use {0} in the string to have it replaced with the first _ parameter numerically first, {1} for second, etc. where trailing digits are sorted numerically to determine order
.PARAMETER title
The title for the dialogue. If specified as an empty string or $null, no title bar is shown
.PARAMETER fullScreen
The dialogue with cover the entire screen (primary monitor)
.PARAMETER screenPercentage
The percentage of the screen resolution to make the dialogue dimensions
.PARAMETER backgroundColour
The colour for the solid background specified as R,G,B values
.PARAMETER textColour
The colour for the text specified as R,G,B values
.PARAMETER fontSize
The font size to use
.PARAMETER fontFamily
The font family to use
.PARAMETER showForSeconds
Show the dialogue for this number of seconds. The default is to show until dismissed
.PARAMETER noClickToClose
A left mouse click on the dialogue will not close the dialogue
.PARAMETER position
The position on the screen to place the dialogue
.PARAMETER notTopmost
Do not make the dialogue system modal
.PARAMETER noClose
Do not allow the window to be closed. It will only close if -showForSeconds is specified and that number of seconds has been reached
.EXAMPLE
& '.\Message user session.ps1' -_clientMetric0 42 -message "Your WiFi signal is weak ({0}%)"
Display a popup in the bottom right hand corner of the session which reads "Your WiFi signal is weak (42%)"
.EXAMPLE
& '.\Message user session.ps1' -message "Please logoff and back on" -title "Message from $env:Username at $(Get-Date -Format G)" -noclose true
Display a popup in the bottom right hand corner of the session with the given text and title. This popup cannot be closed.
.EXAMPLE
& '.\Message user session.ps1' -message "Please logoff and back on" -position Center -screenPercentage 50 -showForSeconds 60 -fontSize 48
Display a popup in the center of the screen in the user's session which is 50% of the screen resolution with the given text, in 48 point, but with no title.
This popup can closed by clicking on the X, pressing Alt F4 or left clicking the popup.
The popup will close automatically after 60 seconds.
.CONTEXT
Session (user session)
.NOTES
Must run in the user's session (or ControlUp console) in order for the WPF code to display a dialogue
Most attributes of the window such as text colour, size, font can be controlled via parameters.
.MODIFICATION_HISTORY:
@guyrleech 2021-05-10 Initial release
@guyrleech 2021-05-12 Fix for colour RGB array values being flattened
@guyrleech 2021-05-13 Formatting codes added to $message
@guyrleech 2022-01-11 Changed [switch] parameters to work with CU console script calling mechanism
#>
[CmdletBinding()]
Param
(
## client metrics (or any parameter passed automagically via CU) must start with an underscore and have the number of the positional parameter in the $message string at the end (which they are sorted on before constructing the message string), eg _clientMetric2
## if not passing any record properties via the SBA definition, delete the parameter completely
$_clientMetric1 ,
[string]$message , ## don't make mandatory in case not passed and CU prompts silently for it
[AllowEmptyString()][AllowNull()]
[string]$title ,
[ValidateSet('Yes', 'No', 'True', 'False')]
[string]$fullScreen = 'No',
[ValidateSet('Yes', 'No', 'True', 'False')]
[string]$noClickToClose = 'No',
[ValidateSet('TopLeft', 'TopRight', 'BottomLeft', 'BottomRight', 'Centre', 'Center')]
[string]$position = 'BottomRight' ,
[ValidateScript({ $_ -gt 0 -and $_ -le 100 })]
[int]$screenPercentage = 25 ,
[string[]]$backgroundColour,
[string[]]$textColour ,
[int]$fontSize = 12 ,
[string]$fontFamily ,
[int]$showForSeconds ,
[ValidateSet('Yes', 'No', 'True', 'False')]
[string]$notTopmost = 'No',
[ValidateSet('Yes', 'No', 'True', 'False')]
[string]$noClose = 'No'
)
[string]$notificationWindowXAML = @'
<Window x:Name="wndMain" x:Class="UserNotifier.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:UserNotifier"
mc:Ignorable="d"
Title="ControlUp Notification" SizeToContent="WidthAndHeight" ResizeMode="NoResize" Background="#CDCDCD">
<TextBlock x:Name="txtblckMain" Grid.Column="2" HorizontalAlignment="Stretch" Margin="50,50,50,50" TextWrapping="Wrap" VerticalAlignment="Stretch" FontFamily="Raleway ExtraBold" FontWeight="Bold" FontSize="36" TextAlignment="Center"/>
</Window>
'@
$VerbosePreference = $(if ( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if ( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if ( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'
Function New-Form {
Param
(
[Parameter(Mandatory = $true)]
$inputXaml
)
$form = $null
$inputXML = $inputXaml -replace 'mc:Ignorable="d"' , '' -replace 'x:N' , 'N' -replace '^<Win.*' , '<Window'
[xml]$xaml = $inputXML
if ( $xaml ) {
$reader = New-Object -TypeName Xml.XmlNodeReader -ArgumentList $xaml
try {
$form = [Windows.Markup.XamlReader]::Load( $reader )
}
catch {
Throw "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .NET is installed.`n$_"
}
$xaml.SelectNodes( '//*[@Name]' ) | ForEach-Object `
{
Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global
}
}
else {
Throw "Failed to convert input XAML to WPF XML"
}
$form
}
if ( [string]::IsNullOrEmpty( $message ) ) {
Throw 'Must specify message text to display'
}
if ( $fullScreen -imatch '^(Yes|True)$' ) {
if ( $PSBoundParameters[ 'percentage' ] -and $percentage -ne 100 ) {
Throw 'Must not specify -percentage when not 100% if -fullscreen also specified'
}
$screenPercentage = 100
}
Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase, System.Windows.Forms
if ( ! ( $mainForm = New-Form -inputXaml $notificationWindowXAML ) ) {
Exit 1
}
if ( $null -ne $backgroundColour -and $backgroundColour.Count -eq 1 -and $backgroundColour[0].IndexOf( ',' ) -ge 0 ) {
$backgroundColour = $backgroundColour -split ','
}
if ( $null -ne $textColour -and $textColour.Count -eq 1 -and $textColour[0].IndexOf( ',' ) -ge 0 ) {
$textColour = $textColour -split ','
}
[hashtable]$messageStrings = @{}
ForEach ( $parameter in $PSBoundParameters.GetEnumerator() ) {
if ( $parameter.Key -match '^_[^\d]*(\d*)$' ) {
try {
$messageStrings.Add( [int]$Matches[1] , $parameter.Value )
}
catch {
Throw "Already have an _ parameter ending in number $($Matches[1]) so can't use $($parameter.Key)"
}
}
}
Write-Verbose -Message "Got $($messageStrings.Count) parameters for message string"
if ( $message -match '\{0\}' -and $messageStrings.Count -eq 0 ) {
Write-Warning -Message "Message string contains {0} but no record properties were passed as parameters"
}
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public class user32
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(String sClassName, String sAppName);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SystemParametersInfo ( uint uiAction, uint uiParam, IntPtr lpvParam, uint fWinIni);
}
'@
[string]$taskbarWindowClass = 'Shell_TrayWnd'
[IntPtr]$hWnd = [user32]::FindWindow( $taskbarWindowClass, $null )
$taskbar = New-Object -TypeName RECT
if ( $hwnd -ne [IntPtr]::Zero ) {
if ( ! [user32]::GetWindowRect( $hWnd , [ref]$taskbar ) ) {
Write-Warning -Message "Failed to get window rect for handle $hWnd"
}
else {
$taskbar | Select-Object -Property * | Write-Verbose
}
}
else {
Write-Warning -Message "Found no window with class "
}
$WPFtxtblckMain.Text = $message -f ($messageStrings.GetEnumerator() | Sort-Object -Property Key | Select-Object -ExpandProperty Value)
Write-Verbose -Message "Expanded message text is `"$($WPFtxtblckMain.Text)`""
$mainForm.WindowStyle = 'ToolWindow'
if ( [string]::IsNullOrEmpty( $title ) ) {
$mainForm.WindowStyle = 'None'
}
else {
$mainForm.Title = $title
}
$mainForm.Topmost = $notTopmost -notmatch '^(yes|true)$'
$primaryScreen = [System.Windows.Forms.Screen]::PrimaryScreen
Write-Verbose -Message "Primary screen $($primaryScreen.WorkingArea.Width) x $($primaryScreen.WorkingArea.Height), working area $($primaryScreen.Bounds.Width) x $($primaryScreen.Bounds.Height)"
[int]$primaryWidth = [math]::Min( $primaryScreen.WorkingArea.Width - $primaryScreen.WorkingArea.X , $primaryScreen.Bounds.Width - $primaryScreen.Bounds.X )
[int]$primaryHeight = [math]::Min( $primaryScreen.WorkingArea.Height - $primaryScreen.WorkingArea.Y , $primaryScreen.Bounds.Height - $primaryScreen.Bounds.Y )
$WPFtxtblckMain.Width = ( $primaryWidth * $screenPercentage / 100 )
$WPFtxtblckMain.Height = ( $primaryHeight * $screenPercentage / 100 )
[int]$width = $WPFtxtblckMain.Width + $WPFtxtblckMain.Margin.Left + $WPFtxtblckMain.Margin.Right
[int]$height = $WPFtxtblckMain.Height + $WPFtxtblckMain.Margin.Top + $WPFtxtblckMain.Margin.Bottom
if ( ! [string]::IsNullOrEmpty( $title ) ) {
$height += [System.Windows.SystemParameters]::CaptionHeight + [System.Windows.SystemParameters]::BorderWidth * 2
$width += [System.Windows.SystemParameters]::BorderWidth * 2
}
if ( $fullScreen -imatch '^(Yes|True)$' ) {
$mainForm.Left = $mainForm.Top = 0
}
else {
[int]$taskbarWidth = [math]::Abs( $taskbar.Right - $taskbar.Left )
[int]$taskbarHeight = [math]::Abs( $taskbar.Bottom - $taskbar.Top )
switch -Regex ( $position ) {
'TopLeft' {
$mainForm.Left = 0
$mainForm.Top = 0
}
'TopRight' {
$mainForm.Left = $primaryWidth - $Width
$mainForm.Top = 0
}
'BottomLeft' {
$mainForm.Left = 0
$mainForm.Top = $primaryHeight - $Height - $taskbarHeight
}
'BottomRight' {
$mainForm.Left = $primaryWidth - $width
$mainForm.Top = $primaryHeight - $height - $taskbarHeight
}
'Centre|Center' {
$mainForm.Left = ( $primaryWidth - $Width ) / 2
$mainForm.Top = ( $primaryHeight - $Height ) / 2
}
}
}
if ( $PSBoundParameters[ 'backgroundColour' ] ) {
if ( $backgroundColour.Count -ne 3 ) {
Throw "Must specify R,G,B values for background colour"
}
$colour = [System.Windows.Media.Color]::FromRgb( $backgroundColour[0] , $backgroundColour[1] , $backgroundColour[2] )
$mainForm.Background = [System.Windows.Media.SolidColorBrush]::new( $colour )
}
if ( $PSBoundParameters[ 'textColour' ] ) {
if ( $textColour.Count -ne 3 ) {
Throw "Must specify R,G,B values for text colour"
}
$colour = [System.Windows.Media.Color]::FromRgb( $textColour[0] , $textColour[1] , $textColour[2] )
$WPFtxtblckMain.Foreground = [System.Windows.Media.SolidColorBrush]::new( $colour )
}
if ( $PSBoundParameters[ 'fontSize' ] ) {
$WPFtxtblckMain.FontSize = $fontSize
}
if ( $PSBoundParameters[ 'fontFamily' ] ) {
$WPFtxtblckMain.FontFamily = $fontFamily
}
$script:formTimer = $null
[bool]$script:timerExpired = $false
[scriptblock]$timerBlock = `
{
$this.Stop()
$script:timerExpired = $true
$mainForm.Close()
}
if ( $PSBoundParameters.ContainsKey( 'showForSeconds' ) -and $showForSeconds -gt 0 ) {
$mainForm.Add_SourceInitialized({
if ( $script:formTimer = New-Object -TypeName System.Windows.Threading.DispatcherTimer ) {
$script:formTimer.Interval = New-TimeSpan -Seconds $showForSeconds
$script:formTimer.Add_Tick( $timerBlock )
$script:formTimer.Start()
}
})
}
if ( $noClickToClose -notmatch '^(Yes|True)$' -and $noClose -notmatch '^(Yes|True)$' ) {
$mainForm.add_MouseLeftButtonUp({
$mainForm.Close()
})
}
if ( $noClose -match '^(Yes|True)$' ) {
$mainForm.add_Closing({
$_.Cancel = ! $script:timerExpired
})
}
Write-Verbose -Message "Dialog $($Width) x $($Height) at $($mainForm.Left) , $($mainForm.Top)"
$mainForm.UpdateLayout()
$result = $mainForm.ShowDialog()
Write-Verbose -Message "Dialog after is at $($mainForm.Left) , $($mainForm.Top)"
if ( $script:formTimer ) {
$script:formTimer.Stop()
$script:formTimer.remove_Tick( $timerBlock )
$script:formTimer = $null
$timerBlock = $null
Remove-Variable -Name timerBlock -Force -Confirm:$false
}
If (!$result) {
Write-Output -InputObject 'User has seen and closed the message.'
}