#CREATE HASHTABLE AND RUNSPACE FOR GUI $WPFGui = [hashtable]::Synchronized(@{ }) $newRunspace = [runspacefactory]::CreateRunspace() $newRunspace.ApartmentState = "STA" $newRunspace.ThreadOptions = "UseNewThread" $newRunspace.Open() $newRunspace.SessionStateProxy.SetVariable("WPFGui", $WPFGui) #Create master runspace andadd code $psCmd = [System.Management.Automation.PowerShell]::Create().AddScript( { # Add WPF and Windows Forms assemblies. This must be done inside the runspace that contains the primary program code. try { Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase, System.Drawing, system.windows.forms, System.Windows.Controls.Ribbon, System.DirectoryServices.AccountManagement } catch { Throw 'Failed to load Windows Presentation Framework assemblies.' } try { Add-Type -Name Win32Util -Namespace System -MemberDefinition @' [DllImport("Kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("User32.dll")] public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow); [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool SwitchToThisWindow(IntPtr hWnd, bool fUnknown); const UInt32 SWP_NOSIZE = 0x0001; const UInt32 SWP_NOMOVE = 0x0002; const UInt32 SWP_NOACTIVATE = 0x0010; const UInt32 SWP_SHOWWINDOW = 0x0040; static readonly IntPtr HWND_BOTTOM = new IntPtr(1); static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); static readonly IntPtr HWND_TOP = new IntPtr(0); static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); public static void SetBottom(IntPtr hWindow) { SetWindowPos(hWindow, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); } public static void SetTop(IntPtr hWindow) { SetWindowPos(hWindow, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); } '@ } catch { Write-Verbose "Win32Util already defined" } #region Utility Functions # This is the list of functions to add to the InitialSessionState that is used for all Asynchronus Runsspaces $SessionFunctions = New-Object System.Collections.ArrayList function Invoke-Async { <# .SYNOPSIS Runs code, with variables, asynchronously .DESCRIPTION This function runs the given code in an asynchronous runspace. This lets you process data in the background while leaving the UI responsive to input .PARAMETER Code The code to run in the runspace .PARAMETER Variables A hashtable containing variable names and values to pass into the runspace .EXAMPLE $AsyncParameters = @{ Variables = @{ Key1 = 'Value1' Key2 = $SomeOtherVariable } Code = @{ Write-Host "Key1: $Key1`nKey2: $Key2" } } Invoke-Async @AsyncParameters .NOTES It's more reliable to pass single values than copmlex objects due to the way PowerShell handles value/reference passing with objects .INPUTS Variables, Code .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ScriptBlock] $Code, [Parameter(Mandatory = $false)] [hashtable] $Variables ) # Add the above code to a runspace and execute it. $PSinstance = [powershell]::Create() #| Out-File -Append -FilePath $LogFile $PSinstance.Runspace = [runspacefactory]::CreateRunspace($InitialSessionState) $PSinstance.Runspace.ApartmentState = "STA" $PSinstance.Runspace.ThreadOptions = "UseNewThread" $PSinstance.Runspace.Open() if ($Variables) { # Pass in the specified variables from $VariableList $Variables.keys.ForEach({ $PSInstance.Runspace.SessionStateProxy.SetVariable($_, $Variables.$_) }) } $PSInstance.AddScript($Code) $PSinstance.BeginInvoke() $WPFGui.Error = $PSInstance.Streams.Error } $SessionFunctions.Add('Invoke-Async') | Out-Null Function New-WPFDialog() { <# .SYNOPSIS This neat little function is based on the one from Brian Posey's Article on Powershell GUIs .DESCRIPTION I re-factored it a bit to return the resulting XaML Reader and controls as a single, named collection. .PARAMETER XamlData XamlData - A string containing valid XaML data .EXAMPLE $MyForm = New-WPFDialog -XamlData $XaMLData $MyForm.Exit.Add_Click({...}) $null = $MyForm.UI.Dispatcher.InvokeAsync{$MyForm.UI.ShowDialog()}.Wait() .NOTES Place additional notes here. .LINK http://www.windowsnetworking.com/articles-tutorials/netgeneral/building-powershell-gui-part2.html .INPUTS XamlData - A string containing valid XaML data .OUTPUTS a collection of WPF GUI objects. #> Param( [Parameter(Mandatory = $True, HelpMessage = 'XaML Data defining a WPF ', Position = 1)] [string]$XamlData, [Parameter(Mandatory = $False, HelpMessage = 'XaML Data defining WPF [CmdletBinding()] param ( [Parameter(ParameterSetName = 'On')] [switch] $On, [Parameter(ParameterSetName = 'Off')] [switch] $Off ) Switch ($PSCmdlet.ParameterSetName) { 'On' { $WPFGui.MainGrid.Effect.Radius = 10 } 'Off' { $WPFGui.MainGrid.Effect.Radius = 0 } } } $SessionFunctions.Add('Set-Blur') | Out-Null function Copy-Object { <# .SYNOPSIS Copies a PSObject .DESCRIPTION Copies an object to new memory. Sometimes you really need to duplicate an object, rather than just create a new pointer to it. This serializes an object then deserializes it to new memory as a brute force mechanism to copy it. .PARAMETER InputObject The Object to be copied .EXAMPLE $NewCopy = Copy-Object $OldObject .NOTES .INPUTS An object .OUTPUTS A copy of an object #> param ( $InputObject ) $SerialObject = [System.Management.Automation.PSSerializer]::Serialize($InputObject) return [System.Management.Automation.PSSerializer]::Deserialize($SerialObject) } $SessionFunctions.Add('Copy-Object') | Out-Null Function New-MessageDialog() { <# .SYNOPSIS Displays a Windows 11 styled MEssage Dialog .DESCRIPTION This is a utility function to display a baisc information, error, or simple input window in a Windows 11 style. .PARAMETER DialogTitle 'Dialog Title' .PARAMETER H1 'Major Header' .PARAMETER DialogText 'Message Text' .PARAMETER CancelText 'Cancel Text' .PARAMETER ConfirmText 'Confirm Text' .PARAMETER Beep 'Plays sound if set' .PARAMETER GetInput 'Shows input TextBox if set' .PARAMETER IsError 'Shows error icon if set' .PARAMETER IsAsync 'Process asynchronously when set' .PARAMETER Owner' 'Owner Window, required when this is a child' .EXAMPLE $NewDialog = @{ DialogTitle = 'Example Dialog' H1 = "This is a pop-up dialog" DialogText = "Dialog text should go here" ConfirmText = 'Continue' GetInput = $false Beep = $true IsError = $true Owner = $WPFGui.UI } $Dialog = New-MessageDialog @NewDialog .NOTES .INPUTS None .OUTPUTS DialogResult, nd Text if requested #> Param( [Parameter(Mandatory = $True, HelpMessage = 'Dialog Title', Position = 1)] [string]$DialogTitle, [Parameter(Mandatory = $True, HelpMessage = 'Major Header', Position = 2)] [string]$H1, [Parameter(Mandatory = $True, HelpMessage = 'Message Text', Position = 3)] [string]$DialogText, [Parameter(Mandatory = $false, HelpMessage = 'Cancel Text', Position = 4)] [string]$CancelText = $null, [Parameter(Mandatory = $True, HelpMessage = 'Confirm Text', Position = 5)] [string]$ConfirmText, [Parameter(Mandatory = $false, HelpMessage = 'Plays sound if set', Position = 6)] [switch]$Beep, [Parameter(Mandatory = $false, HelpMessage = 'Shows input TextBox if set', Position = 7)] [switch]$GetInput, [Parameter(Mandatory = $false, HelpMessage = 'Shows error icon if set', Position = 8)] [switch]$IsError, [Parameter(Mandatory = $false, HelpMessage = 'Process asynchronously when set', Position = 9)] [switch]$IsAsync, [Parameter(Mandatory = $true, HelpMessage = 'Owner Window, required when this is a child', Position = 10)] [PSObject]$Owner ) $file = $PSScriptRoot + ".\DialogPanel.xaml" $xamlData = Get-Content $file -Raw $Dialog = New-WPFDialog -XamlData $xamlData if ($Owner) { if ($IsAsync) { $Owner.Dispatcher.invoke([action] { $Dialog.UI.Owner = $Owner }) } else { $Dialog.UI.Owner = $Owner } } $Dialog.MainWindow.Title = $DialogTitle $Dialog.DialogTitle.Text = $DialogTitle $Dialog.H1.Text = $H1 $Dialog.DialogText.Text = $DialogText if ($CancelText) { $Dialog.CancelButton.Content = $CancelText } else { $Dialog.CancelButton.Visibility = 'hidden' } $Dialog.ConfirmButton.Content = $ConfirmText if ($IsError) { $Dialog.ErrorIcon.Visibility = 'Visible' $Dialog.DialogText.Margin = '8,8,0,0' } if ($GetInput) { $Dialog.Input.Visibility = 'Visible' } $Dialog.Add('Result', [System.Windows.Forms.DialogResult]::Cancel) | Out-Null $Dialog.ConfirmButton.add_Click( { $Dialog.Result = [System.Windows.Forms.DialogResult]::OK $Dialog.UI.Close() }) $Dialog.CancelButton.Add_Click( { $Dialog.Result = [System.Windows.Forms.DialogResult]::Cancel $Dialog.UI.Close() }) $Dialog.UI.add_ContentRendered( { if ($Beep) { [system.media.systemsounds]::Exclamation.play() } }) $null = $Dialog.UI.Dispatcher.InvokeAsync{ $Dialog.UI.ShowDialog() }.Wait() return @{ DialogResult = $Dialog.Result Text = $Dialog.Input.Text } } $SessionFunctions.Add('New-MessageDialog') | Out-Null function Write-Activity { <# .SYNOPSIS Write a colorized entry into the specified RichTextBox .DESCRIPTION This is usually used to write an entry into a RichTexBox which is being used as an Activity Log. .PARAMETER Prefix The "prefix" text, usually program section or activity, to pre-pended to the row .PARAMETER Text The actual text to write .PARAMETER Stream The RichTextBox to write to .PARAMETER IsError If set, this is an error message, write everything in RED .EXAMPLE Write-Activity -Prefix 'PoSH GUI Template' -Text 'Example Activity Log Entry' -Stream 'Output' .NOTES If you change the name of $WPFGui, you'll need to change it here. .INPUTS Text .OUTPUTS None #> param ( # Prefix text, describe which part is printing output [Parameter(Mandatory = $true)] [string] $Prefix, # Text to be printed [Parameter(Mandatory = $true)] [string] $Text, # Output stream to be used [Parameter(Mandatory = $true)] [string] $Stream, [switch] $IsError ) $WPFGui.UI.Dispatcher.Invoke([action] { $DateStamp = Get-Date -Format "yyyy-MM-dd-HH-mm-ss" $TextRun = New-Object System.Windows.Documents.Run $TextRun.Foreground = "Red" $TextRun.Text = $DateStamp $Paragraph = New-Object System.Windows.Documents.Paragraph($TextRun) $TextRun = New-Object System.Windows.Documents.Run $TextRun.Foreground = "#FF9A9A9A" $TextRun.Text = ":" $Paragraph.Inlines.Add($TextRun) # $WPFGui."$Stream".AppendText($TextRun) $TextRun = New-Object System.Windows.Documents.Run $TextRun.Foreground = "#FF0078D7" $TextRun.Text = $Prefix $Paragraph.Inlines.Add($TextRun) # $WPFGui."$Stream".AppendText($TextRun) $TextRun = New-Object System.Windows.Documents.Run $TextRun.Foreground = "#FF9A9A9A" $TextRun.Text = ": " $Paragraph.Inlines.Add($TextRun) # $WPFGui."$Stream".AppendText($TextRun) $TextRun = New-Object System.Windows.Documents.Run if ( $IsError ) { $TextRun.Foreground = "Red" } else { $TextRun.Foreground = "Black" } $TextRun.Text = $Text $Paragraph.Inlines.Add($TextRun) | Out-Null # $WPFGui."$Stream".AppendText($TextRun) $WPFGui."$Stream".Document.Blocks.Add($Paragraph) | Out-Null $WPFGui."$Stream".ScrollToEnd() }) } $SessionFunctions.Add('Write-Activity') | Out-Null function Clear-Activity { <# .SYNOPSIS Clears the specified RichTextBox .DESCRIPTION clear the activity log of a RichTextBox .PARAMETER Stream The RichTextBox to clear to .EXAMPLE Clear-Activity -Stream 'Output' .NOTES If you change the name of $WPFGui, you'll need to change it here. .INPUTS Text .OUTPUTS None #> param ( # Output stream to be used [Parameter(Mandatory = $true)] [string] $Stream ) $WPFGui.UI.Dispatcher.Invoke([action] { $WPFGui."$Stream".Document.Blocks.Clear() | Out-Null }) } $SessionFunctions.Add('Clear-Activity') | Out-Null function Write-StatusBar { <# .SYNOPSIS Write text, and a progress percentage, to the StatusBar area .DESCRIPTION Writes Status text and progress to the StatusBar area. .PARAMETER Progress Progress Bar value, from 0-100 .PARAMETER Text Status Text to display .EXAMPLE Write-StatusBar -Progress 25 -Text "We're a quarter of the way there." .NOTES If you change the name of $WPFGui, you'll need to change it here. .INPUTS Text .OUTPUTS None #> param ( # Prefix text, describe which part is printing output [Parameter(Mandatory = $true)] [int] $Progress, # Text to be printed [Parameter(Mandatory = $true)] [string] $Text ) $WPFGui.UI.Dispatcher.invoke([action] { $WPFGui.Progress.Value = $Progress $WPFGui.StatusText.Text = $Text }) } $SessionFunctions.Add('Write-StatusBar') | Out-Null function Save-Screenshot { <# .SYNOPSIS Save a Screenshot of the specified screen(s) .DESCRIPTION Save a screenshot of the specified screen(s) in the specified format. Valid formats are BMP, JPG/JPEG, and PNG. .PARAMETER FilePath Filename to save to .PARAMETER Format An optional parameter to specify a specific format. If not set, then the format is guessed from FilePath .PARAMETER ScreenNumber An int specifying which screen to save. .PARAMETER AllScreens A switch to specify a capture of all screens. .EXAMPLE Save-ScreenShot -FilePath "ScreenShot.jpg" -Format 'JPG' -ScreenNumber = 0 .NOTES If you change the name of $WPFGui, you'll need to change it here. .INPUTS None .OUTPUTS Screenshot #> param ( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $false)] [ValidateSet("BMP", "JPG", "JPEG", "PNG", "Unspecified")] [string]$Format = "Unspecified", [Parameter(Mandatory = $false)] [ValidateScript({ ((0 -le $_) -and ( $_ -le (([System.Windows.Forms.Screen]::AllScreens).Count - 1) )) })] [int16]$ScreenNumber, [Parameter(Mandatory = $false)] [Switch]$AllScreens ) $ScreenList = [System.Windows.Forms.Screen]::AllScreens $Top = 0 $Bottom = 0 $Left = 0 $Right = 0 if ($AllScreens) { foreach ($CurrentScreen in $ScreenList) { $Bounds = $CurrentScreen.Bounds if ($Top -gt $Bounds.Top) { $Top = $Bounds.Top } if ($Left -gt $Bounds.Left) { $Left = $Bounds.Left } if ($Bottom -lt $Bounds.Bottom) { $Bottom = $Bounds.Bottom } if ($Right -lt $Bounds.Right) { $Right = $Bounds.Right } } $Width = $Right - $Left $Height = $Bottom - $Top } else { $Left = $ScreenList[$ScreenNumber].Bounds.Left $Top = $ScreenList[$ScreenNumber].Bounds.Top $Right = $ScreenList[$ScreenNumber].Bounds.Right $Bottom = $ScreenList[$ScreenNumber].Bounds.Bottom $Width = $ScreenList[$ScreenNumber].Bounds.Width $Height = $ScreenList[$ScreenNumber].Bounds.Height } $Bounds = [Drawing.Rectangle]::FromLTRB($Left, $Top, $Right, $Bottom) $Bitmap = New-Object Drawing.Bitmap $Width, $Height $Graphics = [Drawing.Graphics]::FromImage($Bitmap) $Graphics.CopyFromScreen($Bounds.Location, [Drawing.Point]::Empty, $Bounds.Size) try { if ($Format -eq 'Unspecified') { $Format = $FilePath.Split('.')[-1] } } catch { Write-Error "Unable to determine filetype from $FilePath" } $Bitmap.Save($FilePath, [System.Drawing.Imaging.ImageFormat]::$Format) $Graphics.Dispose() $Bitmap.Dispose() } $SessionFunctions.Add('Save-Screenshot') | Out-Null Function Get-FileName() { <# .SYNOPSIS Use a Win32 FileDialog to request a Filename from the user. .DESCRIPTION Shows a Win32 FileOpenDialog or FleSaveDialog to request a Filename from the User .PARAMETER Title The window Title .PARAMETER Filter The FileType filter, see Microsoft's documentation on this. .PARAMETER InitialDirectory The Initial Directory to display .PARAMETER FileName The default FileName to select .PARAMETER Save A switch to indicate that this is a SAVE dialog and not an OPEN dialog. .EXAMPLE $FileNameParameters = @{ Title = 'New Log File Name' Filter = 'LOG Files (*.LOG)|*.log|HTML Files (*.html)|*.html|RTF Files (*.rtf)|*.rtf' FileName = "$(Get-Date -Format 'yyyy-MM-dd-HHmmss').log" Save = $true } $FileName = Get-FileName @FileNameParameters .NOTES .INPUTS Text .OUTPUTS FileName #> Param( [Parameter()][string]$Title = 'Open File', [Parameter()][string]$Filter = 'All Files (*.*)|*.*', [Parameter()][string]$InitialDirectory = "$($env:HOMEDRIVE)$($env:HOMEPATH)", [Parameter()][string]$FileName = 'File.log', [switch]$Save ) $Result = $false # Setup and open an "Open File" Dialog If ( $Save ) { $FileDialog = New-Object Microsoft.Win32.SaveFileDialog $FileDialog.FileName = $FileName } else { $FileDialog = New-Object Microsoft.Win32.OpenFileDialog } $FileDialog.Title = $Title $FileDialog.filter = $Filter $FileDialog.initialDirectory = $InitialDirectory $FileDialog.AddExtension = $true $FileDialogResult = $FileDialog.ShowDialog($owner) #"OK clicked" status $DialogOK = [System.Windows.Forms.DialogResult]::OK if ($FileDialogResult -eq $DialogOK) { $Result = $FileDialog } return $Result.FileName } $SessionFunctions.Add('Get-FileName') | Out-Null Function Get-FolderName() { <# .SYNOPSIS (Ab)Use a Win32 FileOpenDialog to request a FolderName from the user. .DESCRIPTION Shows a Win32 FileOpenDialog to request a FolderName from the User .EXAMPLE $FolderName = Get-FolderName .NOTES .INPUTS None .OUTPUTS FolderName #> $Result = $false # Setup and open an "Open File" Dialog $FileDialog = New-Object Microsoft.Win32.OpenFileDialog $FileDialog.Filter = "Select Folder|(Select Folder)" $FileDialog.Title = "Select Folder" $FileDialog.FileName = "Select Folder" $FileDialog.CheckFileExists = $false $FileDialog.ValidateNames = $false $FileDialog.CheckPathExists = $true $FileDialogResult = $FileDialog.ShowDialog() if ($FileDialogResult) { $Result = $FileDialog.FileName.Replace('Select Folder', '') } return $Result } $SessionFunctions.Add('Get-FolderName') | Out-Null function ConvertFrom-FlowDocument { <# .SYNOPSIS Converts a FlowDocument to HTML .DESCRIPTION Converts a (simple) RichTextBox FlowDocument to HTML .PARAMETER Document The FlowDocument to Convert .PARAMETER Title The HTML page Title to use .PARAMETER Save A switch to indicate that this is a SAVE dialog and not an OPEN dialog. .EXAMPLE $html = ConvertFrom-FlowDocument -Document $Document -Title $Title .NOTES .INPUTS FlowDocument and Title .OUTPUTS HTML document #> param ( [Parameter(Mandatory = $True, HelpMessage = 'System.Windows.Documents.FlowDocument to convert to HTML', Position = 1)] [System.Windows.Documents.FlowDocument]$Document, [Parameter(Mandatory = $false, HelpMessage = 'Document Title', Position = 2)] [string]$Title = "" ) $html = @" $Title `n $($Document.Blocks.ForEach({ $_.Inlines.Foreach({" $($_.Text)"}) "
`n" })) "@ return $html } $SessionFunctions.Add('ConvertFrom-FlowDocument') | Out-Null function Save-FlowDocument { <# .SYNOPSIS Saves a RichTextBox FlowDocument in the requested format - LOG, RTF, or HTML. .DESCRIPTION Saves a RichTextBox FlowDocument as Plain Text (LOG), RichText (RTF), or HTML .PARAMETER Document FlowDocument to Save .PARAMETER Format The File Format to use - TXT, RTF, or HTML .PARAMETER Title The HTML page Title to use .PARAMETER FileName The name of the file to write. .EXAMPLE Save-FlowDocument -Document $WPFGui.Output.Document -Format $Format -FileName $FileName -Title "Example logs $(Get-Date -Format 'yyyy-MM-dd-HH-mm-ss')" .NOTES .INPUTS FlowDocument and Title .OUTPUTS HTML document #> param ( [Parameter(Mandatory = $True, HelpMessage = 'Windows FlowDocument', Position = 1)] [System.Windows.Documents.FlowDocument]$Document, [Parameter(Mandatory = $True, HelpMessage = 'Format to save - txt, html, or rtf', Position = 2)] [ValidateSet("TXT", "RTF", "HTML")] [string]$Format, [Parameter(Mandatory = $false, HelpMessage = 'HTML Document Title', Position = 3)] [string] $Title, [Parameter(Mandatory = $True, HelpMessage = 'Filename', Position = 4)] [string] $FileName ) # The TextRange is used for the TXT and RTF formats because it has a built-in .Save() method. $TextRange = [System.Windows.Documents.TextRange]::new($Document.ContentStart, $Document.ContentEnd) # This is used for all three $FHand = [System.IO.FileStream]::new($FileName, 'OpenOrCreate') switch ($Format) { 'txt' { $TextRange.Save($FHand, [System.Windows.DataFormats]::Text) } 'html' { # Convert the FlowDocument to HTML, cast it to a collection of bytes and write it to FHand. $html = ConvertFrom-FlowDocument -Document $Document -Title $Title $htmlBytes = [byte[]][char[]]$html $FHand.Write($htmlBytes, 0, $htmlBytes.Count) } 'rtf' { $TextRange.Save($FHand, [System.Windows.DataFormats]::Rtf) } } $FHand.Flush() $FHand.Close() } $SessionFunctions.Add('Save-FlowDocument') | Out-Null # Create an Initial Session State for the ASync runspace and add all the functions in $SessionFunctions to it. $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $SessionFunctions.ForEach({ $SessionFunctionEntry = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new($_, (Get-Content Function:\$_)) $InitialSessionState.Commands.Add($SessionFunctionEntry) | Out-Null }) #endregion Utility Functions #region - Setup default values # Development Mode Toggle $DevMode = $true # Failure Sentry. We watch this to know whether we need to bail out before trying to show the main window. $Failed = $false if ($DevMode) { # If DevMode is set, we should read the UI definitions from these paths. This is useful when editing the XaML in Blend or Visual Studio. $XaMLWindowPath = $PSScriptRoot + '.\MainWindow.xaml' $XaMLResourceDictionaryPath = $PSScriptRoot + '.\ControlTemplates.xaml' # Load the UI definition. $WPFXaML = Get-Content -Raw -Path $XaMLWindowPath $ResourceXaML = Get-Content -Raw -Path $XaMLResourceDictionaryPath # If DevMode is set, override normal pwd/cwd detection and set it explicitly. $ScriptPath = "C:\Users\ME050696\Desktop\Powershell\Test" } else { if ($MyInvocation.MyCommand.CommandType -eq "ExternalScript") { $ScriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition } else { $ScriptPath = Split-Path -Parent -Path ([Environment]::GetCommandLineArgs()[0]) if (!$ScriptPath) { $ScriptPath = "." } } $mainWindow = $PSScriptRoot + ".\MainWindow.xaml" $WPFXaML = Get-Content $mainWindow -Raw $mainWindow = $PSScriptRoot + ".\ControlTemplates.xaml" $ResourceXaML = Get-Content $mainWindow -Raw } #endregion #region Build the GUI try { $WPFGui = [hashtable]::Synchronized( (New-WPFDialog -XamlData $WPFXamL -Resources $ResourceXaML) ) $WPFGui.Add('ResourceXaML', $ResourceXaML) } catch { $failed = $true } $WPFGui.Add('hWnd', $null) $WPFGui.Add('AsyncResult', [PSCustomObject]@{ Success = $false Message = "" }) #endregion #region Titlebar buttons $WPFGui.MinimizeButton.add_Click( { $WPFGui.UI.WindowState = 'Minimized' }) $WPFGui.RestoreButton.add_Click( { $WPFGui.UI.WindowState = 'Normal' $WPFGui.MaximizeButton.Visibility = 'Visible' $WPFGui.RestoreButton.Visibility = 'Collapsed' }) $WPFGui.MaximizeButton.add_Click( { $WPFGui.UI.WindowState = 'Maximized' $WPFGui.MaximizeButton.Visibility = 'Collapsed' $WPFGui.RestoreButton.Visibility = 'Visible' }) $WPFGui.CloseButton.add_Click( { $WPFGui.UI.Close() }) #endregion Titlebar buttons #region Menu Buttons $WPFGui.SaveLogs.add_Click( { # When clicked, save the Activity Log to a file. $FileNameParameters = @{ Title = 'New Log File Name' Filter = 'LOG Files (*.LOG)|*.log|HTML Files (*.html)|*.html|RTF Files (*.rtf)|*.rtf' FileName = "$(Get-Date -Format 'yyyy-MM-dd-HHmmss').log" Save = $true } $FileName = Get-FileName @FileNameParameters if ( $FileName ) { $Extension = ([System.IO.FileInfo]$FileName).Extension $Format = $Extension.Replace('.', '').Replace('log', 'txt') Save-FlowDocument -Document $WPFGui.Output.Document -Format $Format -FileName $FileName -Title "PoSH GUI Template logs $(Get-Date -Format 'yyyy-MM-dd-HH-mm-ss')" } $WPFGui.MenuButton.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) }) $WPFGui.MenuAllgemein.add_Click({ $WPFGUI.DeploymentWindow.Title = "PI V7 - Allgemein" $WPFGUI.TabControl.SelectedIndex = 0 $WPFGui.MenuButton.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) }) $WPFGui.MenuComputer.add_Click({ $WPFGUI.DeploymentWindow.Title = "PI V7 - Computer" $WPFGUI.TabControl.SelectedIndex = 1 $WPFGui.MenuButton.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) }) $WPFGui.MenuBenutzer.add_Click({ $WPFGUI.DeploymentWindow.Title = "PI V7 - Benutzer" $WPFGUI.TabControl.SelectedIndex = 2 $WPFGui.MenuButton.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) }) $WPFGui.MenuExit.add_Click({ $WPFGui.UI.Close() }) #endregion #region Main Program buttons and fields $WPFGui.MenuOpen.add_Completed( { # Flip the end points of the menu animation so that it will open when clicked and close when clicked again $AnimationParts = @('MenuToggle', 'BurgerFlipper', 'BlurPanel') foreach ($Part in $AnimationParts) { $To = $WPFGui."$Part".To $From = $WPFGui."$Part".From $WPFGui."$Part".To = $From $WPFGui."$Part".From = $To } }) $WPFGui.UI.add_ContentRendered( { # Once the window is visible, grab handle to it if ( $WPFGui.hWnd -eq $null) { $WPFGui.hWnd = (New-Object System.Windows.Interop.WindowInteropHelper($WPFGui.UI)).Handle } [System.Win32Util]::SetTop($WPFGui.hWnd) # Write an example log entry Clear-Activity -Stream 'Output' Write-Activity -Prefix 'PoSH GUI Template' -Text 'Example Activity Log Entry' -Stream 'Output' }) #endregion # This region is unique to the example UI and is meant to demonstrate how to code-behind the various controls #region Setup items for static comboxes # The best way to bind data to list controls - ComboBoxes, ListBoxes, DataGrids, et. al. - is to use an ObservableCollection # Create an ObservableCollection for the example DataGrid $WPFGui.Add('ExampleGridItemsList', (New-Object System.Collections.ObjectModel.ObservableCollection[PSCustomObject]) ) # Set the ObservableCollection as the ItemsSource for the DataGrid $WPFGui.ExampleGrid.ItemsSource = $WPFGUI.ExampleGridItemsList # Some sample data for the grid, in CSV format $ExampleGridItems = @' "CheckBox","Description","Filename","ExtraInfo","RowIsValid" "True","Lorem ipsum dolor sit","amet.xslx","consectetur adipiscing elit","True" "False","sed do eiusmod tempor","incididunt.txt","ut labore et dolore magna aliqua.","True" "True","Ut enim ad minim veniam","quis.doc","nostrud exercitation ullamco","False" '@ | ConvertFrom-Csv # Add each row to the the ObservableCollection. The DataGrid will displat this data automatically $ExampleGridItems.Foreach({ $WPFGui.ExampleGridItemsList.Add($_) | Out-Null }) $WPFGui.Add('ComputerGridItemsList', (New-Object System.Collections.ObjectModel.ObservableCollection[PSCustomObject]) ) $WPFGui.ComputerGrid.ItemsSource = $WPFGUI.ComputerGridItemsList $ComputerGridItems = @' "Ping","Computername","Anwender","CheckPing" "True","ME0001","NW000000 - Nachname, Vorname", "False" "False","C36PCTA70000001","NW000001 - Nachname1, Vorname1","False" "True","C36PCTA70000002","NW000002 - Nachname2, Vorname2","False" "False","C36PCTA70000003","NW000003 - Nachname3, Vorname3","False" '@ | ConvertFrom-Csv # Add each row to the the ObservableCollection. The DataGrid will displat this data automatically $ComputerGridItems.Foreach({ $WPFGui.ComputerGridItemsList.Add($_) | Out-Null }) $WPFGui.ComputerGrid.Add_SelectedCellsChanged({ # $rowData = $WPFGui.ComputerGrid.CurrentItem.Computername # Write-Activity -Stream 'OutputComputer' -Prefix 'GridList' -Text "$rowData" # $ComputerGridItems.Foreach({ # # $test = Test-Connection ME0002 -Count 1 -Quiet -ErrorAction SilentlyContinue # Write-Activity -Stream 'OutputComputer' -Prefix 'GridList' -Text "$($_.Ping)" # }) }) # $WPFGui.ComputerGridItemsList.CollectionChanged({ # Write-Activity -Stream 'OutputComputer' -Prefix 'ComputerGridItemsList' -Text "2123" # }) $WPFGui.Add('MonitorTimer', (New-Object System.Windows.Threading.DispatcherTimer) ) $WPFGUI.MonitorTimer.Interval = [timespan]::FromSeconds(2) $WPFGUI.MonitorTimer.Add_Tick({ # $item = $WPFGui.ComputerGrid.CurrentItem # $item.Ping = Test-Connection -ComputerName $item.Computername -Quiet # $item.Ping = $true $WPFGui.ComputerGridItemsList | Foreach-Object { # $_.Ping = Test-Connection "ME0001" -Quiet -Count 1 -AsJob # $_.Ping = [bool](0..1 |Get-Random) } $WPFGui.ComputerGrid.Items.Refresh() #Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "$($WPFGui.ComputerGridItemsList.Items(0).Ping)" # $item = $WPFGui.ComputerGridItemsList.Items(0).Ping }) $WPFGui.Ping.add_Click({ # Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "Click Ping" # $test = [System.Linq.Enumerable]::Single($WPFGui.ComputerGridItemsList.Items, [Func[object,object]]{ param($x) $x.Computername -eq "ME0001" }) # Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "$($WPFGui.ComputerGridItemsList.Items(0))" # $result = $WPFGui.ComputerGridItemsList | Where-Object { $_.Computername -eq "ME0001" } # $result.Ping = !$result.Ping # Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "$($result.Ping)" if ($WPFGUI.MonitorTimer.IsEnabled) { $WPFGUI.MonitorTimer.Stop() Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "dispatcherTimer Stop" } else { $WPFGUI.MonitorTimer.Start() Write-Activity -Stream 'OutputComputer' -Prefix 'Ping' -Text "dispatcherTimer Start" } }) # The ComboBoxes work similarly to the DataGrid. $WPFGui.Add('ComboBox1List', (New-Object System.Collections.ObjectModel.ObservableCollection[string]) ) $WPFGui.ComboBox1.ItemsSource = $WPFGui.ComboBox1List foreach ($BoxItem in ('laboris nisi ut aliquip').Split(' ')) { $WPFGUI.ComboBox1List.Add([string]$BoxItem) | Out-Null } $WPFGui.ComboBox1.Items.Refresh() $WPFGui.Add('ComboBox2List', (New-Object System.Collections.ObjectModel.ObservableCollection[string]) ) $WPFGui.ComboBox2.ItemsSource = $WPFGui.ComboBox2List foreach ($BoxItem in ('ex ea commodo consequat').Split(' ')) { $WPFGUI.ComboBox2List.Add([string]$BoxItem) | Out-Null } $WPFGui.ComboBox2.Items.Refresh() # Defaulted values for other input types $WPFGUI.Add('TextBox1Text', "dolor") $WPFGUI.TextBox1.Text = $WPFGUI.TextBox1Text $WPFGui.Add('pwd', $ScriptPath) $WPFGui.Add('DomainList', (New-Object System.Collections.ObjectModel.ObservableCollection[string])) foreach ($Domain in @('Duis aute irure').Split(' ')) { $WPFGUI.DomainList.Add($Domain) | Out-Null } $WPFGUI.UserDomain.ItemsSource = $WPFGUI.DomainList # Not used here, but included for completeness. This is a ButtonClick event that can be routed to a given button on the window. $WPFGui.Add('ButtonClick', (New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) $WPFGUI.SetPath.Add_Click({ # Show a Folder Selection dialog when clicked. $Parameters = @{ Title = 'Select the folder containing explorer.exe' } $ExplorerPath = Get-FolderName @Parameters $ExplorerFile = 'explorer.exe' if ( -not (Test-Path (Join-Path $ExplorerPath $ExplorerFile))) { $WPFGui.TextBox2.Foreground = "#FFFF0000" } else { $WPFGui.TextBox2.Foreground = "#FF000000" } $WPFGui.TextBox2.Text = $(Join-Path $ExplorerPath $ExplorerFile) }) $WPFGui.Execute.add_Click({ # Run this code when execute is clicked. A sample dialog is shown synchronously, then the variable values are printed asynchronously. Set-Blur -On $NewDialog = @{ DialogTitle = 'Example Dialog' H1 = "This is a pop-up dialog" DialogText = "Dialog text should go here" ConfirmText = 'Continue' GetInput = $false Beep = $true IsError = $false Owner = $WPFGui.UI } New-MessageDialog @NewDialog # $Dialog = New-MessageDialog @NewDialog Set-Blur -Off $AsyncParameters = @{ Variables = @{ WPFGui = $WPFGui ComboBox1Value = $WPFGui.ComboBox1.SelectedValue ComboBox2Value = $WPFGui.ComboBox2.SelectedValue CheckBox1Check = $WPFGui.CheckBox1.IsChecked TextBox1Text = $WPFGui.TextBox1.Text TextBox2Text = $WPFGui.TextBox2.Text Domain = $WPFGui.UserDomain.SelectedValue Username = $WPFGui.UserName.Text SecurePassword = $WPFGui.Password.SecurePassword GridDataJSON = $WPFGui.ExampleGrid.Items | ConvertTo-Json } Code = { Write-Activity -Stream 'Output' -Prefix 'Example' -Text "ComboBox1 Value: $ComboBox1Value" Write-StatusBar -Progress 12.5 -Text "ComboBox1 Value" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "ComboBox2 Value: $ComboBox2Value" Write-StatusBar -Progress 25 -Text "ComboBox2 Value" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "CheckBox1 is $(if (-not $CheckBox1Check) { "not " })checked." Write-StatusBar -Progress 37.5 -Text "TextBox1 Value" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "TextBox1 Text: $TextBox1Text" Write-StatusBar -Progress 50 -Text "TextBox1 Value" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "TextBox2 Text: $TextBox2Text" Write-StatusBar -Progress 62.5 -Text "TextBox2 Value" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "Credential Values: $Domain\$Username $SecurePassword" Write-StatusBar -Progress 87.5 -Text "Credential Values: $Domain\$Username $SecurePassword" # Start-Sleep -Seconds 1 Write-Activity -Stream 'Output' -Prefix 'Example' -Text "DataGrid Values (as JSON) $GridDataJSON" Write-StatusBar -Progress 100 -Text "DataGrid Values (as JSON)" # Start-Sleep -Seconds 1 Write-StatusBar -Progress 0 -Text "Ready." } } Invoke-Async @AsyncParameters }) #endregion if ( -not $Failed ) { # Setup async runspace items $WPFGui.Host = $host $WPFGui.Add('Runspace', [runspacefactory]::CreateRunspace($InitialSessionState)) $WPFGui.Runspace.ApartmentState = "STA" $WPFGui.Runspace.ThreadOptions = "ReuseThread" $WPFGui.Runspace.Open() $WPFGui.UI.Dispatcher.InvokeAsync{ $WPFGui.UI.ShowDialog() }.Wait() $WPFGui.Runspace.Close() $WPFGui.Runspace.Dispose() } }) $psCmd.Runspace = $newRunspace $psCmd.Invoke()