Private/Wissen/C_Advance/C13a_GUI.ps1

<#
 
# GUI
 
GUI (Graphical User Interface) zu deutsch ein Windows-Fenster als Benutzerschnittstelle. Mittels .NET können verschiedene GUI's programmiert werden. Hierzu gehören u.a. Web-Interfaces via .ASPX, WinForm oder WPF um moderne Fenster für Windows-System zu erzeugen. PowerShell hat vollen Zugriff auf .NET und so können auch Windows-Fenster aus PowerShell heraus erzeugt werde.
 
Rechnet Euros in Nicht-Euro-Währungen gem. den Vorgaben der EZB.
 
- **Hashtags** CLR WPF WinForm GUI Window Fenster
 
- **Version** 2020.6.12
 
#>


# READ Eine Variante um mit PowerShell eine GUI zu erstellen bietet WPF und XAML. Das Layout kann dabei ähnlich einer HTML-Datei erstellt werden. Das kostenlose [Visual Studio Express](https://visualstudio.microsoft.com/de/vs/express/) bietet zudem die Möglichkeit, das Layout einfach in einem grafischen Editor zu erstellen. Der eigentliche Programmcode wird mit PowerShell Script umgesetzt. Diese Beispiel demonstriert eine GUI für das Cmdlet AKPT\Get-EuroExchange. Dieses Skript ist autark lauffähig da der Code für Get-EuroExchange unter 5. implementiert ist. Daher ist ein installieren und importieren des Module AKPT nicht nötig.

#region 1. Benötigte Modules importieren

# ! Hier werden Module importiert die nicht standardmäßig mit der PowerShell installiert werden um Dritten zu zeigen welche Module evtl. zuerst installiert werden müssten.

using module Microsoft.PowerShell.Management
using module Microsoft.PowerShell.Utility

#endregion

#region 2. Benötigte Namespaces bekannt machen

# ! Für eine übersichtlichere Schreibweise im weiteren Verlauf können die benötigten Namespaces bekannt gemacht werde.

using namespace System.Management.Automation
using namespace System.PresentationFramework
using namespace System.Windows.Markup

#endregion

#region 3. Laufzeitverhalten des Scripts festlegen

# ! Im Fehlerfall sollte das Script genau an dieser Stelle stoppen und um u.a. Tippfehler bei Variablennamen zu vermeiden die Zuweisung vor der ersten Verwendung eingeschaltet.

$BackupErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = [ActionPreference]::Stop
Set-StrictMode -Version 'Latest'

#endregion

#region 4. Benötigte Assemblies laden

# ! Assemblies die nicht standardmäßig geladen werden müssen manuell geladen werden.

Add-Type -AssemblyName 'PresentationFramework' # Für WPF

#endregion

#region 5. Benötigte private Funktionen / Cmdlets definieren

# ! Für dieses Cmdlet Get-EuroExchange soll im weiteren Verlauf eine GUI erstellt werden.

function Get-EuroExchange {
    <#
    .SYNOPSIS
    EZB-Währungsrechner
     
    .DESCRIPTION
    Rechnet Euros in Nicht-Euro-Währungen gem. den Vorgaben der EZB.
     
    .INPUTS
    System.String mit Währungssymbolen
    PSCustomObject mit folgenden Properties Currency <string> und Euros <decimal>
     
    .OUTPUTS
    PSCustomObject
     
    .PARAMETER Currency
    Währungssymbol der EZB.
     
    .PARAMETER Euros
    Euro-Betrag der umgerechnet werden soll.
     
    .PARAMETER ListCurrency
    Eine Übersicht aller möglichen Währungssymbole.
     
    .EXAMPLE
    Get-EuroExchange -Currency USD
    Ermittelt den Wechselkurs für US-Dollar
     
    .EXAMPLE
    Get-EuroExchange -Currency USD -Euros 100
     
    .EXAMPLE
    Get-EuroExchange -ListCurrency
     
    .EXAMPLE
    "USD", "RUB", "AUD" | Get-EuroExchange
     
    .EXAMPLE
    "USD", "RUB", "AUD" | Get-EuroExchange -Euros 100
     
    .EXAMPLE
    "USD,10", "RUB,100", "AUD,1000" | ConvertFrom-Csv -Header Currency, Euros | Get-EuroExchange
    #>

    [CmdletBinding(DefaultParameterSetName = 'Calculate')]
    param (
        [Parameter(ParameterSetName = "Calculate", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'GBP', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'USD', 'ZAR')]
        [Alias("Währung")]
        [string]$Currency,

        [Parameter(ParameterSetName = "Calculate", ValueFromPipelineByPropertyName = $true)]
        [ValidateRange(0.0001, 2147483647)]
        [Alias("Euronen")]
        [decimal]$Euros = 1,

        [Parameter(ParameterSetName = "Overview", Mandatory=$true)]
        [switch]$ListCurrency
    )
    begin {
        [datetime]$StartTime = Get-Date

        #region Update local cache and read it
        
        # ! Build Cachefile:
        [string]$EuroExchangeCacheFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'EcbEuroExchangeCache.xml'

        # ! Exist Cachefile read timestamp:
        [timespan]$ECBCacheDifferenceSpan = New-TimeSpan -Hours 999
        if ((Test-Path -Path $EuroExchangeCacheFile)) {
            'ECB-EuroExchange-Cache-File found!' | Write-Verbose
            [xml]$EuroExchangeContent = Get-Content -Path $EuroExchangeCacheFile
            [datetime]$EuroExchangeTime = $EuroExchangeContent.Envelope.Cube.Cube | Select-Object -ExpandProperty 'time'
            [timespan]$ECBCacheDifferenceSpan = (Get-Date) - $EuroExchangeTime
        }

        # ! Is Cache-Difference-TimeSpan greater 39h than update from ECB:
        if($ECBCacheDifferenceSpan.TotalHours -ge 39) {
            'The ECB-EuroExchange-Cache-File is updated because the file was not found or is older than 39 hours.' | Write-Verbose
            Invoke-WebRequest -Uri "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" | Select-Object -ExpandProperty Content | Set-Content -Path $EuroExchangeCacheFile -Force
        }

        # ! Read Cachefile for the next stapes:
        [xml]$EuroExchangeContent = Get-Content -Path $EuroExchangeCacheFile
        $EuroExchangeCubes = $EuroExchangeContent.Envelope.Cube.Cube.Cube
        "ECB-EuroExchange-Cache-File from $($EuroExchangeContent.Envelope.Cube.Cube | Select-Object -ExpandProperty 'time') read it." | Write-Verbose

        #endregion

        switch ($PSCmdlet.ParameterSetName) {
            'Overview' {
                'Get-EuroExchange works in Overview-Mode.' | Write-Verbose
                $EuroExchangeCubes | ForEach-Object -Process { [PSCustomObject]@{ Currency = $_.currency } } | Sort-Object -Property 'Currency'
            }
            'Calculate' {
                'Get-EuroExchange works in Calculate-Mode.' | Write-Verbose
            }
        }
    }
    process {
        if($PSCmdlet.ParameterSetName -eq 'Calculate') {
            [decimal]$CurrencyRate = $EuroExchangeCubes | Where-Object -Property 'currency' -EQ -Value $Currency | Select-Object -ExpandProperty 'rate'
            [PSCustomObject]@{
                Currency    = $Currency.ToUpper()
                Rate        = $CurrencyRate
                Euros       = $Euros
                SumCurrency = $CurrencyRate * $Euros
            }
        }
    }
    end {
        [timespan]$Duration = (Get-Date) - $StartTime
        "Done in $($Duration.TotalMilliseconds) ms!" | Write-Verbose
    }
}

#endregion

#region 6. Ereignis-Routinen

# ! Vor dem öffnen des Fensters bzw. vor dem ersten Auslösen eines Steuerelement-Events müssen dessen Routinen deklariert werden.

function EuroRateCalculate([string]$Currency, [double]$Euros) {
    try {
        $Result = Get-EuroExchange -Currency $Currency -Euros $Euros
        [double]$Rate        = $Result.Rate
        [double]$SumCurrency = $Result.SumCurrency
    }
    catch {
        [double]$Rate        = [double]::NaN
        [double]$SumCurrency = [double]::NaN
    }
    $Script:My.RateControl.Text  = "{0:#,##0.0000} {1}" -f $Rate       , $Currency
    $Script:My.SummeControl.Text = "{0:#,##0.0000} {1}" -f $SumCurrency, $Currency
}
#endregion

#region 7. WPF Window erzeugen

# ! Nun gilt das WPF-Window zu materialisieren.

$Script:My = [HashTable]::Synchronized(@{})

# Die XAML-WPF-Window-Struktur läßt sich am einfachsten mittels Visual Studio Express im Designer erzeugen, siehe dazu auch C13b_VisualStudio_Handout.png.
$Script:My.WindowXaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="€ CALCULATOR"
    Width="336"
    Height="220"
    FontFamily="Consolas"
    FontSize="14"
    WindowStartupLocation="CenterScreen">
    <Viewbox
        Margin="15"
        Stretch="Uniform"
        StretchDirection="Both">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="AUTO" />
                <RowDefinition MinHeight="10" />
                <RowDefinition MinHeight="40" />
                <RowDefinition MinHeight="40" />
                <RowDefinition MinHeight="40" />
            </Grid.RowDefinitions>
            <ComboBox
                x:Name="WährungenControl"
                Grid.Row="0"
                Grid.Column="0"
                VerticalAlignment="Top"
                HorizontalContentAlignment="Center"
                FontSize="20"
                SelectedValue="USD" />
            <TextBlock
                Grid.Row="0"
                Grid.Column="1"
                Margin="10,0,0,0"
                VerticalAlignment="Center">
                Währungssymbol
            </TextBlock>
            <TextBox
                x:Name="RateControl"
                Grid.Row="2"
                Grid.Column="0"
                VerticalAlignment="Center"
                FontWeight="Bold"
                IsReadOnly="True"
                TextAlignment="Right" />
            <TextBlock
                Grid.Row="2"
                Grid.Column="1"
                Margin="10,0,0,0"
                VerticalAlignment="Center"
                FontWeight="Bold">
                Rate
            </TextBlock>
            <TextBox
                x:Name="EurosControl"
                Grid.Row="3"
                Grid.Column="0"
                VerticalAlignment="Center"
                TextAlignment="Right" />
            <TextBlock
                Grid.Row="3"
                Grid.Column="1"
                Margin="10,0,0,0"
                VerticalAlignment="Center">
                € (EUR)
            </TextBlock>
            <TextBox
                x:Name="SummeControl"
                Grid.Row="4"
                Grid.Column="0"
                MinWidth="195"
                VerticalAlignment="Center"
                FontWeight="Bold"
                IsReadOnly="True"
                TextAlignment="Right" />
            <TextBlock
                Grid.Row="4"
                Grid.Column="1"
                Margin="10,0,0,0"
                VerticalAlignment="Center"
                FontWeight="Bold">
                SUMME = Rate * €
            </TextBlock>
        </Grid>
    </Viewbox>
</Window>
'@


# Das Window-Objekt wird über die XAML-Struktur erstellt:
$Script:My.Window = [XamlReader]::Parse($Script:My.WindowXaml)

# Aus dem Window-Objekt werden die benötigten Steuerelemente über das Attribute X:Name lokalisiert und referenziert:
$Script:My.WährungenControl = $Script:My.Window.FindName('WährungenControl')
$Script:My.RateControl      = $Script:My.Window.FindName('RateControl')
$Script:My.EurosControl     = $Script:My.Window.FindName('EurosControl')
$Script:My.SummeControl     = $Script:My.Window.FindName('SummeControl')

# Die ComboBox erhält ihre Werte die vom Benutzer ausgewählt werden können:
Get-EuroExchange -ListCurrency | ForEach-Object -Process { $Script:My.WährungenControl.Items.Add($_.Currency) | Out-Null }

# Die ComboBox stößt die Berechnung der Ausgabe (Rate, Summe) an, wenn die Auswahl wechselt:
$Script:My.WährungenControl.Add_SelectionChanged({ EuroRateCalculate -Currency $Script:My.WährungenControl.SelectedItem -Euros $Script:My.EurosControl.Text })

# Das erste Element der ComboBox wird ausgewählt:
$Script:My.WährungenControl.SelectedIndex = 0

# Die TextBox stößt die Berechnung der Ausgabe (Rate, Summe) an, wenn der Euro-Text sich ändert:
$Script:My.EurosControl.Add_TextChanged({ EuroRateCalculate -Currency $Script:My.WährungenControl.SelectedItem -Euros $Script:My.EurosControl.Text })

# In der TextBox wird der Default-Wert auf 1 Euro gesetzt:
$Script:My.EurosControl.Text = "1"

# Das WPF-Window wird angezeigt:
$Script:My.Window.Topmost = $true
$Script:My.Window.ShowDialog() | Out-Null

#endregion

$ErrorActionPreference = $BackupErrorActionPreference

# TIPP Egal was Sie vorhaben bleibt in grunde der Ablauf gleich. Bedenken Sie jedoch das das Wissen über WPF und OOP nicht unerheblich ist und einen gro0en Teil ausmacht.