|
<# |
|
.Synopsis |
|
Loads and shows a window xaml file and attaches handlers as well as providing all elements to be used |
|
|
|
.Description |
|
Version 1.0.0 |
|
License ISC |
|
(c) Nabil Redmann 2019 - 2023, Powershell 5+ (including pwsh 7) |
|
|
|
.Notes |
|
based on https://stackoverflow.com/a/52416973/1644202 |
|
#> |
|
|
|
$manifest = @{ |
|
"ModuleVersion" = "1.0.0" |
|
"Author" = "Nabil Redmann" |
|
"CompanyName" = "Nabil Redmann" |
|
"Description" = "Loads and shows a window xaml file and attaches handlers as well as providing all elements to be used" |
|
"Copyright" = "(c) Nabil Redmann 2019 - 2023" |
|
"RootModule" = "XAML.GUI-framework.psm1" |
|
"Path" = "XAML.GUI-framework.psd1" |
|
"GUID" = "be0f512e-413d-4308-8e0f-dec08fe7b522" |
|
"CompatiblePSEditions" = @("Desktop") |
|
#"FunctionsToExport" = @() |
|
#"CmdletsToExport" = @() |
|
#"VariablesToExport" = "" |
|
#"AliasesToExport" = @() |
|
} |
|
# New-ModuleManifest $manifest |
|
|
|
|
|
# Enable visual styles, in case there will be a message box or alike |
|
Function Enable-VisualStyles |
|
{ |
|
Add-Type -AssemblyName System.Drawing,System.Windows.Forms |
|
[System.Windows.Forms.Application]::EnableVisualStyles() |
|
} |
|
|
|
|
|
Function New-Window |
|
{ |
|
Param |
|
( |
|
[Parameter(ValueFromPipeline=$True,Mandatory=$True)] $xamlFile |
|
) |
|
|
|
$xamlString = Get-Content -Path $xamlFile |
|
return New-WindowXamlString($xamlString) |
|
} |
|
|
|
Function New-WindowUrl |
|
{ |
|
Param |
|
( |
|
[Parameter(ValueFromPipeline=$True,Mandatory=$True)] $url |
|
) |
|
|
|
$xamlString = (New-Object System.Net.WebClient).DownloadString($url) |
|
return New-WindowXamlString($xamlString) |
|
} |
|
|
|
|
|
$knownEvents = $Null |
|
|
|
Function Add-KnownEvents |
|
{ |
|
Param ( [String[]] $EventNames ) |
|
|
|
$knownEvents = $EventNames |
|
} |
|
|
|
|
|
Function New-WindowXamlString |
|
{ |
|
Param |
|
( |
|
[Parameter(Mandatory=$true)] $xamlString |
|
) |
|
|
|
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase |
|
|
|
If (!$knownEvents) { $knownEvents = @( |
|
# Some major events. There are way more. |
|
# Window |
|
"Initialized", "Loaded", "Unloaded", "Activated", "Closed", "Closing", "GotFocus", "LostFocus", "SizeChanged", |
|
# Checkbox, Buttons etc |
|
"Click", "Checked", "MouseDoubleClick", "MouseEnter", "MouseLeave", "MouseDown", "MouseUp", "MouseLeftButtonDown", "MouseLeftButtonUp", "MouseRightButtonDown", "MouseRightButtonUp", "MouseMove", "MouseWheel", |
|
# Text |
|
"KeyDown", "KeyUp", "PreviewKeyDown", "PreviewKeyUp", |
|
# Combobox |
|
"SelectionChanged" |
|
) } |
|
|
|
# store window class |
|
$match = [Regex]::Match($xamlString, '(?s)^<Window[^>]*(x:Class="([^"]*)")', [System.Text.RegularExpressions.RegexOptions]::Multiline) |
|
|
|
if ($match.Success -eq $False) { |
|
Write-Host 'XAML.GUI: XAML does not contain a <Window x:Class="...">' |
|
Exit 4 |
|
} |
|
$windowClass = $match.Captures.Groups[2].Value |
|
|
|
#=========================================================================== |
|
# fix XAML markup for powershell |
|
#=========================================================================== |
|
$xamlString = $xamlString -replace $match.Captures.Groups[1].Value,'' -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace "x:n",'N' -replace "x:Bind", "Binding" |
|
try { |
|
[xml]$XAML = $xamlString |
|
} |
|
catch { |
|
Write-Host "XAML.GUI: XAML parsing error" -ForegroundColor Red |
|
Write-Host $_.Exception.message -ForegroundColor Red |
|
Exit 5 |
|
} |
|
|
|
#=========================================================================== |
|
# storing events |
|
#=========================================================================== |
|
$eventElements = @() |
|
Foreach ($event in $knownEvents) { |
|
Foreach ($node in $XAML.SelectNodes("//*[@$event]")) { |
|
If (!$node.Attributes['Name']) { |
|
# Needed, because XAML elements will later be matched to the pure XML by name to append the event to the parsed XAML Element |
|
Write-Host "XAML.GUI: Adding NAME to element with event" $node.OuterXml |
|
$name = $node.LocalName + "_" + $(New-Guid) -replace "-","_" #$(Get-Random) |
|
$node.SetAttribute("Name", $name) |
|
|
|
Write-Host " ... Applied new generated Name = $($node.Name)" |
|
|
|
<#*NONAME -- works now above |
|
Write-Host "XAML.GUI: Name not set for element $($node.Name) with event $event and function $($node.$event)" -ForegroundColor Red |
|
Write-Host " " $node.OuterXml -ForegroundColor Red |
|
# Exit 3 |
|
#> |
|
} |
|
|
|
#*NONAME If ($node.Attributes['Name']) { |
|
$eventElements += @{ |
|
e = $node |
|
ev = $event |
|
fn = $node.$event |
|
name = $node.Attributes['Name'].Value |
|
} |
|
#} |
|
|
|
# PS does not handle events, need to be removed, but were added to the elements collection |
|
$node.RemoveAttribute($event) |
|
} |
|
} |
|
|
|
|
|
#=========================================================================== |
|
#Read XAML |
|
#=========================================================================== |
|
$reader = (New-Object System.Xml.XmlNodeReader $XAML) |
|
|
|
try { $Form = [Windows.Markup.XamlReader]::Load($reader) } |
|
catch [System.Management.Automation.MethodInvocationException] { |
|
Write-Warning "XAML.GUI: We ran into a problem with the XAML code. Check the syntax for this control..." |
|
Write-Host $error[0].Exception.Message -ForegroundColor Red |
|
Exit 1 |
|
} |
|
catch {#if it broke some other way |
|
Write-Host "XAML.GUI: Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed." |
|
Exit 2 |
|
} |
|
|
|
|
|
#=========================================================================== |
|
# attaching click handlers |
|
#=========================================================================== |
|
Write-Host "XAML.GUI: Window class is $windowClass " |
|
Foreach ($evData in $eventElements) { |
|
$fnName = $evData.fn |
|
if ($windowClass) { |
|
$fnName = "$windowClass.$fnName" |
|
} |
|
|
|
$fns = Get-ChildItem function: | Where-Object { $_.Name -like $fnName } # function namespace.windowclassname.function_name($Sender, $EventArgs) |
|
|
|
if ($evData.name) { $name = $evData.name } else { $name = '-no name-' } |
|
If (!$fns.Count) { |
|
Write-Host "XAML.GUI: Linking event $($evData.ev) on element $name -> function $fnName(`$Sender,`$EventArgs) FAILED: no handler" -ForegroundColor Red |
|
} |
|
else { |
|
Write-Host "XAML.GUI: Linking event $($evData.ev) on element $name -> function $fnName(`$Sender,`$EventArgs)" |
|
|
|
Invoke-Expression ('$Form.FindName($evData.name).Add_' + $evData.ev + '( $fns[0].ScriptBlock )') |
|
} |
|
} |
|
|
|
#=========================================================================== |
|
# Store named elements to be acessable through $Elements |
|
#=========================================================================== |
|
$Elements = @{} |
|
#$XAML.SelectNodes("//*[@Name]") | %{Set-Variable -Name "GUI_$($_.Name)" -Value $Form.FindName($_.Name)} |
|
$XAML.SelectNodes("//*[@Name]") | %{ $Elements[$_.Name] = $Form.FindName($_.Name) } |
|
|
|
$global:e = $Elements |
|
|
|
$Elements["_Window"] = $Form |
|
|
|
return $Elements,$Form |
|
} |
|
|
|
# allow `New-Window $XamlFile | Show-Window` by checking for _Window |
|
Function Show-Window { |
|
Param( |
|
[Parameter(ValueFromPipeline=$True,Mandatory=$True)] $window, |
|
$dialog = $True |
|
) |
|
|
|
$win = $window |
|
if ($window._Window) { |
|
$win = $window._Window |
|
} |
|
|
|
if ($dialog) { |
|
$win.ShowDialog() | Out-Null |
|
} |
|
else { |
|
$win.Show() | Out-Null |
|
} |
|
$global:win = $win |
|
} |
|
|
|
|
|
# .Net methods for hiding/showing the console in the background, https://stackoverflow.com/a/40621143/1644202 |
|
Add-Type -Name Window -Namespace Console -MemberDefinition ' |
|
[DllImport("Kernel32.dll")] |
|
public static extern IntPtr GetConsoleWindow(); |
|
|
|
[DllImport("user32.dll")] |
|
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow); |
|
' |
|
|
|
Function Show-Console |
|
{ |
|
Param([Parameter(Mandatory=$false)] $state=4) |
|
|
|
$consolePtr = [Console.Window]::GetConsoleWindow() |
|
|
|
# https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-showwindow |
|
# Hide = 0, |
|
# ShowNormal = 1, |
|
# ShowMinimized = 2, |
|
# ShowMaximized = 3, |
|
# Maximize = 3, |
|
# ShowNormalNoActivate = 4, |
|
# Show = 5, |
|
# Minimize = 6, |
|
# ShowMinNoActivate = 7, |
|
# ShowNoActivate = 8, |
|
# Restore = 9, |
|
# ShowDefault = 10, |
|
# ForceMinimized = 11 |
|
|
|
[Console.Window]::ShowWindow($consolePtr, $state) |
|
} |
|
|
|
Function Hide-Console |
|
{ |
|
# return true/false |
|
$consolePtr = [Console.Window]::GetConsoleWindow() |
|
#0 hide |
|
[Console.Window]::ShowWindow($consolePtr, 0) |
|
} |
|
|
|
|
|
|
|
|
|
Function New-ClonedObjectStruct |
|
{ |
|
param([PSCustomObject]$srcObject) |
|
|
|
return $srcObject.psobject.copy() # | ConvertTo-Json -depth 100 | ConvertFrom-Json |
|
} |
|
|
|
# cheap prop access without error (for any type of object |
|
Function Get-PropOrNull |
|
{ |
|
param($thing, [string]$prop) |
|
|
|
Try { |
|
$thing.$prop |
|
} Catch {} |
|
} |
|
|
|
|
|
# https://gist.github.com/nwolverson/8003100 |
|
Function Get-VisualChildren($item) |
|
{ |
|
for ($i = 0; $i -lt [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($item); $i++) { |
|
$child = [System.Windows.Media.VisualTreeHelper]::GetChild($item, $i) |
|
Get-VisualChildren($child) |
|
} |
|
$item |
|
} |
|
|
|
Function Get-CellItemByName |
|
{ |
|
Param( |
|
[ref]$Parent, |
|
$ItemNo, |
|
$Name |
|
) |
|
|
|
[System.Windows.Forms.Application]::DoEvents() |
|
|
|
$Parent.value | Write-Host |
|
|
|
$items = (Get-VisualChildren ($Parent.Value) | ? { $_.GetType().Name -eq "ListViewItem" }) |
|
|
|
$items | Write-Host |
|
$ItemNo | Write-Host |
|
$items[$ItemNo] | Write-Host |
|
|
|
Get-VisualChildren $items[$ItemNo] | Write-Host |
|
|
|
|
|
return (Get-VisualChildren $items[$ItemNo] | ? { $_.Name -eq $Name} | Select-Object -First 1) |
|
} |
|
|
|
|
|
Function Wait-AwaitJob |
|
{ |
|
Param |
|
( |
|
[Parameter(Mandatory=$true)] $job |
|
) |
|
|
|
while ($job.state -eq 'Running') { |
|
[System.Windows.Forms.Application]::DoEvents() # keep form responsive |
|
} |
|
|
|
# Captures and throws any exception in the job output -> '-ErrorAction stop' --- otherwise returns result |
|
return Receive-Job $job -ErrorAction Continue |
|
} |
|
|
|
# start and await a job |
|
Function Start-AwaitJob |
|
{ |
|
Param |
|
( |
|
[Parameter(Mandatory=$true)] $scriptBlock, |
|
[Parameter(Mandatory=$false)] $ArgumentList=@(), |
|
[Parameter(Mandatory=$false)] $Dir, # sets the current working directory (use it to set the subfolder) ! |
|
[Parameter(Mandatory=$false)] $await = $True |
|
) |
|
|
|
$useDir = $PWD |
|
If ($Dir) { $useDir = Resolve-Path $Dir } |
|
|
|
$job = Start-Job -Init ([ScriptBlock]::Create("Set-Location '$($useDir -replace "'", "''")'")) -ScriptBlock $scriptBlock -ArgumentList $ArgumentList |
|
|
|
if ($await) { |
|
return Wait-AwaitJob $job |
|
} |
|
else { |
|
return $job |
|
} |
|
} |
|
|
|
|
|
Function Show-MessageBox |
|
{ |
|
Param([string]$Message="This is a default Message.", |
|
[string]$Title="Default Title", |
|
[ValidateSet("Asterisk","Error","Exclamation","Hand","Information","None","Question","Stop","Warning")] |
|
[string]$Type="Error", |
|
[ValidateSet("AbortRetryIgnore","OK","OKCancel","RetryCancel","YesNo","YesNoCancel")] |
|
[string]$Buttons="OK" |
|
) |
|
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") |
|
$MsgBoxResult = [System.Windows.Forms.MessageBox]::Show($Message,$Title,[Windows.Forms.MessageBoxButtons]::$Buttons,[Windows.Forms.MessageBoxIcon]::$Type) |
|
Return $MsgBoxResult |
|
} |
|
|
|
|
|
Function Invoke-BalloonTip |
|
{ |
|
<# |
|
.Synopsis |
|
Display a balloon tip message in the system tray. |
|
.Description |
|
This function displays a user-defined message as a balloon popup in the system tray. This function |
|
requires Windows Vista or later. |
|
.Parameter Message |
|
The message text you want to display. Recommended to keep it short and simple. |
|
.Parameter Title |
|
The title for the message balloon. |
|
.Parameter MessageType |
|
The type of message. This value determines what type of icon to display. Valid values are |
|
.Parameter SysTrayIcon |
|
The path to a file that you will use as the system tray icon. Default is the PowerShell ISE icon. |
|
.Parameter Duration |
|
The number of seconds to display the balloon popup. The default is 1000. |
|
.Inputs |
|
None |
|
.Outputs |
|
None |
|
.Notes |
|
NAME: Invoke-BalloonTip |
|
VERSION: 1.0 |
|
AUTHOR: Boe Prox |
|
#> |
|
[CmdletBinding()] |
|
Param ( |
|
[Parameter(Mandatory=$True,HelpMessage="The message text to display. Keep it short and simple.")] |
|
[string]$Message, |
|
[Parameter(HelpMessage="The message title")] |
|
[string]$Title="Attention $env:username", |
|
[Parameter(HelpMessage="The message type: Info,Error,Warning,None")] |
|
[System.Windows.Forms.ToolTipIcon]$MessageType="Info", |
|
|
|
[Parameter(HelpMessage="The path to a file to use its icon in the system tray")] |
|
[string]$SysTrayIconPath='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe', |
|
[Parameter(HelpMessage="The number of milliseconds to display the message.")] |
|
[int]$Duration=1000 |
|
) |
|
|
|
If (-NOT $global:balloon) { |
|
$global:balloon = New-Object System.Windows.Forms.NotifyIcon |
|
#Mouse double click on icon to dispose |
|
[void](Register-ObjectEvent -InputObject $balloon -EventName MouseDoubleClick -SourceIdentifier IconClicked -Action { |
|
#Perform cleanup actions on balloon tip |
|
Write-Verbose 'Disposing of balloon' |
|
$global:balloon.dispose() |
|
Unregister-Event -SourceIdentifier IconClicked |
|
Remove-Job -Name IconClicked |
|
Remove-Variable -Name balloon -Scope Global |
|
}) |
|
} |
|
#Need an icon for the tray |
|
$path = Get-Process -id $pid | Select-Object -ExpandProperty Path |
|
#Extract the icon from the file |
|
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($SysTrayIconPath) |
|
#Can only use certain TipIcons: [System.Windows.Forms.ToolTipIcon] | Get-Member -Static -Type Property |
|
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]$MessageType |
|
$balloon.BalloonTipText = $Message |
|
$balloon.BalloonTipTitle = $Title |
|
$balloon.Visible = $true |
|
#Display the tip and specify in milliseconds on how long balloon will stay visible |
|
$balloon.ShowBalloonTip($Duration) |
|
Write-Verbose "Ending function" |
|
} |
|
|
|
|
|
Function Select-FolderDialog |
|
{ |
|
Param( |
|
[string]$Description="Select Folder", |
|
[string]$Path="Desktop", |
|
[string]$RootFolder="Desktop" |
|
) |
|
|
|
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null |
|
|
|
$objForm = New-Object System.Windows.Forms.FolderBrowserDialog |
|
$objForm.Rootfolder = $RootFolder |
|
$objForm.SelectedPath = $Path |
|
$objForm.Description = $Description |
|
$Show = $objForm.ShowDialog() |
|
If ($Show -eq "OK") |
|
{ |
|
Return $objForm.SelectedPath |
|
} |
|
Else |
|
{ |
|
#Write-Error "Operation cancelled by user." |
|
Return '' |
|
} |
|
} |
|
# $folder = Select-FolderDialog # the variable contains user folder selection |
|
|
|
Function Select-FileDialog |
|
{ |
|
Param( |
|
[string]$Title="Select Folder", |
|
[string]$Path="Desktop", |
|
[string]$Filter='Images (*.jpg, *.png)|*.jpg;*.png', |
|
[boolean]$Multiselect=$false |
|
) |
|
|
|
Add-Type -AssemblyName System.Windows.Forms |
|
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{ |
|
Filter = $Filter # Specified file types |
|
Multiselect = $Multiselect # Multiple files can be chosen |
|
} |
|
$FileBrowser.Title = $Title |
|
$FileBrowser.InitialDirectory = $Path |
|
|
|
[void]$FileBrowser.ShowDialog() |
|
|
|
$file = $FileBrowser.FileName; |
|
|
|
If ($FileBrowser.FileNames -like "*\*") { |
|
|
|
# Do something |
|
Return $FileBrowser.FileName #Lists selected files (optional) |
|
} |
|
|
|
else { |
|
#Write-Host "Cancelled by user" |
|
Return "" |
|
} |
|
} |