Skip to content

Instantly share code, notes, and snippets.

@BananaAcid
Last active July 9, 2023 00:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BananaAcid/0484b11a03c03f172740096e213d1d82 to your computer and use it in GitHub Desktop.
Save BananaAcid/0484b11a03c03f172740096e213d1d82 to your computer and use it in GitHub Desktop.
Load Visual-Studio generated XAML windows in powershell and attach events

XAML.GUI framework

Usage

To use it in your own scripts, just load it as module, to make the function available

Module based

New-Module -Name "XAMLGUI" -ScriptBlock ([Scriptblock]::Create((New-Object System.Net.WebClient).DownloadString("https://gist.githubusercontent.com/BananaAcid/0484b11a03c03f172740096e213d1d82/raw/XAML.GUI-framework.ps1")))

as included script

Invoke-Expression ((New-Object System.Net.WebClient).DownloadString("https://gist.githubusercontent.com/BananaAcid/0484b11a03c03f172740096e213d1d82/raw/XAML.GUI-framework.ps1"))

Notes

WinUI3 not supported.

Use Visual Studio 2022, create a WPF-Application (XAML, Desktop), to create a XAML file containing a <Window>.

<#
Example: MINI
#>
. .\XAML.GUI-framework.ps1
Enable-VisualStyles
Hide-Console | Out-Null
function ProjectTest1.MainWindow.btnHelloWorld_Click($Sender, $EventArgs) {
Show-MessageBox "Hello World!"
}
New-Window .\MainWindow.xaml | Show-Window
<#
Example
#>
Param(
$XamlFile = ".\gui.xaml"
)
# this will not allow autobinding of handlers (unless they are in the same scope as with sourcing the ps1 below)
#Import-Module -Name .\XAML.GUI-framework.ps1 -Force -Verbose
# this will allow binding handlers like `function Updater.MainWindow.DoSoftwareDir_Click($Sender, $EventArgs) { ... }`
. .\XAML.GUI-framework.ps1
# pretty up message boxes
Enable-VisualStyles
# early config handling
Hide-Console | Out-Null
#$selectedFolder = Select-FolderDialog
#Invoke-BalloonTip "Example message"
## Binding
# Events like 'Click', 'MouseDown' and more (as in $knownEvents), will be auto attached, if XAML.GUI is invoked in the same scope.
# That means it needs the script to be invoked with ` . ./main.ps1` (sourcing) not just ` ./main.ps1` --- or load the module by `. ./XAML.GUI-framework.ps1` and not Import-Module
# The handlers for this have to be available before invoking New-Window.
# Otherwise, you can attach your Events yourself on all found elements in $Elements, which is returned from New-Window.
# auto add handler by window class name and element Event
# <Window x:Class="Updater.MainWindow" ...>
# <Label HorizontalAlignment="Right" MouseDown="LbCopy_MouseDown" Cursor="Hand" Foreground="Blue">© Nabil Redmann 2019</Label>
function Updater.MainWindow.LbCopy_MouseDown($Sender, $EventArgs) {
Show-MessageBox "0"
}
# Load the main gui, get an array of $elements (as in $knownEvents), and a handle to the form itself
$Elements,$MainWindow = New-Window $XamlFile
# DEBUG *ELEMENTS:
Write-Host "`n All `$Elements.* :" ; $Elements | Format-Table
# Add a handler to an initialized window by element's name
# <Button x:Name="doInstall" Content="run all" Width="75" VerticalAlignment="Center" />
$Elements.doInstall.Add_Click({
Show-MessageBox "1"
})
Write-Host "Waiting for main window to close."
$MainWindow | Show-Window
<#
.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 ""
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment