Comment créer une application graphique avec PowerShell et WPF ?
Sommaire
I. Présentation
Avec PowerShell vous avez découvert comment créer des interfaces graphiques avec PowerShell et Windows Forms, dans un article précèdent. Aujourd’hui, nous allons découvrir comment créer des applications avec Windows Presentation Framework aka WPF.
Nous parlerons de la syntaxe à adopter, des outils qu'il est possible d'utiliser pour créer une interface graphique avec WPF et quelques exemples vont vous permettre de vous faire la main. Avant de commencer, voici le lien vers le tutoriel évoqué ci-dessus :
En fin d'article, retrouvez un lien vers notre GitHub afin de récupérer les sources des projets WPF.
II. Le langage XAML
Apparu en 2006 avec l’arrivée du .Net 3.0, XAML permet la création d’applications riches, et vise à « remplacer » Windows Forms. XAML (prononcé Zammel), acronyme d’eXtensible Application Markup Language, est un langage déclaratif basé sur du XML.
Pour ceux disposant de connaissances en programmations de sites web, vous remarquerez qu'en termes de structure, XAML est également proche du HTML.
XAML constitue la partie principale de votre application, celle qui permettra d’afficher votre interface. C’est votre XAML qui contiendra tout ce que vous souhaitez afficher dans votre interface : boutons, zones de saisie, listes déroulantes, tableaux affichant des données, etc.
- Généralisation d’un contrôle WPF :
<NomduControl></ NomduControl>
- Exemple d’un contrôle de type « Button » en WPF.
<Button></Button>
Attention : le langage XAML est sensible à la casse, cela signifie que les majuscules sont importantes. La preuve en image avec le code suivant où nous définissions deux « TextBox », cependant le second n’est pas écrit correctement.
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<StackPanel Orientation="vertical" VerticalAlignment="Center">
<TextBox Width="140" Height="25" Margin="0 5 5 5">Correct</TextBox>
<Textbox Width="140" Height="25" Margin="0 5 5 5">IT Connect WPF</Textbox>
</StackPanel>
</Grid>
</Page>
Nous observons que seul le premier TextBox est affiché, car le second n’est pas écrit correctement, d’où le message d’erreur.
A. Ces avantages
Le XAML offre différents avantages par rapport à ce que l'on peut faire avec Windows Forms. En effet, la lecture du code est bien plus simple et plus facile à mettre à jour. Le fonctionnement et les syntaxes utilisées permettent de s'y retrouver plus facilement.
B. Comment fonctionne-t-il ?
C’est un langage déclaratif, car son fonctionnement repose, comme pour le XML, sur l’ajout de balises et de tag. Ainsi, une balise permet d’ouvrir un nouvel élément et doit donc être ouverte et fermée.
Chaque élément de votre interface, bouton (Button), zone de saisie (TextBox) est nommé Control. Dans XAML, un Control se traduit sous la forme d’une balise ouverte et fermée respectant une syntaxe bien précise.
- Le non-respect de cette syntaxe entraînera des erreurs.
- À ces Controls, l’ajout d’attributs permettra de mieux gérer l’interface.
C. Les Controls
Parmi les Controls les plus courants, nous retrouvons ceux ci-dessous :
- Label : affichage d’un simple texte
- TextBox : zone de saisie de texte
- PasswordBox : zone de saisie de mots de passe
- ComboBox : liste déroulante permettant d’afficher différents choix, choix unique
- RadioButton : case à cocher, choix unique
- CheckBox : case à cocher, choix multiple
- DataGrid : tableau permettant d’afficher des données
D. Quel outil pour faire du XAML ?
Pour créer une interface en WPF (Windows Presentation Foundation), nous pouvons utiliser une multitude de logiciels, cela va du simple éditeur de texte tel que Notepad++, à Visual Studio Code ou Visual Studio Community ou entreprise. Nous pouvons aussi citer un outil libre : Kaxaml, qui est disponible sur le dépôt GitHub d'un Microsoft MVP nommé Jan Karger.
L’avantage de Kaxaml, c’est qu’il est gratuit, léger et portable et nous le verrons plus en détail, nous avons le rendu en temps réel. Il supporte aussi des références pour l’ajout de thèmes (pour les thèmes, il suffit d’ajouter des références dans Kaxaml et d’ajouter les DLL du thème souhaité.).
Ce qui donne :
- References : dans cette section, nous avons la possibilité de venir ajouter des dépendances à des Librairies de Windows DLL (Dynamic Link Library)
- Snippets : les snippets nous permettent d'ajouter des Controls comme des Textblock, Label, StackPanel.
- Find : cette partie nous permet d'effectuer une recherche dans notre code XAML
- Color Picker : un objet qui nous permet de choisir la couleur et ainsi obtenir le code en Hexadécimal de la couleur précédemment sélectionnée.
- Snapshot : nous permet de prendre une photo de la partie rendue de Kaxaml.
- Xaml Scrubber : nous permet de parcourir le code Xaml et de le formater, de lui appliquer une mise en page.
- Settings : correspond aux paramètres de l'éditeur.
E. Comment utiliser Kaxaml ?
Une fois Kaxaml lancé, vous disposez de trois parties au sein de l’interface graphique :
- Bloc N°1 : correspond à la partie du XAML ou nous allons ajouter nos controls
- Bloc N°2 : correspond à la partie résultats, nous verrons controls apparaitre au sein de cette fenêtre de rendu.
- Bloc N°3 : correspond à la partie outils de Kaxaml.
Essayons de réaliser une interface avec Kaxaml. Voici une vidéo de démonstration pour vous aider, ce sera plus pratique.
III. PowerShell et XAML
Pour rappel, XAML contiendra l’ensemble des controls et notre script PowerShell, quant à lui, nous permettra de définir les actions ou les évènements que nous aurons la possibilité de faire sur les controls.
A. Comment les organiser ?
Les deux parties XAML et PS1 peuvent s’utiliser de deux façons :
- Deux fichiers distincts (méthode que je recommande pour une meilleure lisibilité et maniabilité).
- Tout inclure dans le fichier PowerShell.
Dans un premier temps, nous allons faire deux fichiers distincts :
- Un fichier XAML avec ce code :
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="380"
Height="200"
Title="IT Connect WPF">
<Grid>
<StackPanel HorizontalAlignment="Center" Margin="0,10,0,0" Orientation="Vertical">
<Label HorizontalAlignment="Center" Content="Active Directory" FontSize="20"/>
<GroupBox Width="300" Height="65" Margin="0,10,0,0" Header="Active Directory">
<StackPanel HorizontalAlignment="Center" Margin="0,0,0,0" Orientation="Horizontal">
<Button Name="MonBouton" Width="80" Height="20" Content="Search"/>
<ComboBox Name="MonTextBox" Width="200" Height="20"/>
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</Window>
- Un fichier PowerShell
Il doit contenir une partie qui permettra à PowerShell d’interpréter le fichier XAML.
[xml]$MonXAML = get-content -path "MonProjet.xaml"
$Reader=(New-Object System.Xml.XmlNodeReader $MonXAML)
$Interface = [Windows.Markup.XamlReader]::Load($Reader)
$Interface.ShowDialog() | Out-Null
Que fait ce code ?
- Nous allons d'abord transformer notre XAML en un objet XML en utilisant System.Xml.XmlNodeReader
- Ensuite, nous chargeons ce dernier en utilisant [Windows.Markup.XamlReader]::Load
- Enfin, nous affichons notre interface en utilisant $Interface.ShowDialog()
Cependant, avec Windows PowerShell, nous sommes obligés d’ajouter comme Assembly « presentationframework », qui se présente sous la forme d’un fichier DLL. C’est cette Assembly qui contiendra tous les Controls que nous voulons utiliser. C’est également par elle que notre interface WPF pourra s’afficher. Nous devons ajouter la ligne suivante :
Add-Type -AssemblyName 'presentationframework'
B. Comment exécuter notre application ?
Pour exécuter notre application il suffit d’exécuter notre script PowerShell "monprojet.ps1".
PS> .\monprojet.ps1
Le résultat doit être le suivant :
C. Comment interagir avec les controls ?
Pour interagir avec nos controls, nous devons retrouver les éléments à l’aide de l’attribut « Name » de chaque controls.
Prenons l’exemple du control de notre Button « Search » dans notre code PowerShell. Nous allons chercher les objets et en fonction de leurs noms, nous allons pouvoir mapper une variable PowerShell.
[xml]$MonXAML = get-content -path "MonProjet.xaml"
$Reader=(New-Object System.Xml.XmlNodeReader $MonXAML)
$Interface = [Windows.Markup.XamlReader]::Load($Reader)
$MonBouton = $Interface.FindName("MonBouton")
$MonTextBox = $Interface.FindName("MonComboBox")
Ici, dans notre script PowerShell, la variable « $MonBouton » est mappée à l’objet qui a comme nom « Monbouton » avec la ligne suivante : $MonBouton = $Interface.FindName("MonBouton"). C’est bien, mais le souci dans notre cas, nous n’avons que deux variables, mais nous pouvons faire beaucoup mieux avec un peu de Scripting pour générer les variables.
$Global:pathPanel= split-path -parent $MyInvocation.MyCommand.Definition
function LoadXaml ($filename){
$XamlLoader=(New-Object System.Xml.XmlDocument)
$XamlLoader.Load($filename)
return $XamlLoader
}
$XamlMainWindow=LoadXaml("$Global:pathPanel\MonProjet.xaml")
$reader = (New-Object System.Xml.XmlNodeReader $XamlMainWindow)
$Form = [Windows.Markup.XamlReader]::Load($reader)
$XamlMainWindow.SelectNodes("//*[@Name]") | %{
try {Set-Variable -Name "$("WPF_"+$_.Name)" -Value $Form.FindName($_.Name) -ErrorAction Stop}
catch{throw}
}
Function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){ Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true }
Write-Host "Found the following interactable elements from our form" -ForegroundColor Cyan
Get-Variable *WPF*
}
Get-FormVariables
Que fait ce code ?
- Dans la variable « $XamlMainWindow » nous utilisons la méthode « SelectNodes » avec la propriété « Name » et pour chaque objet, nous créons une variable qui aura comme nom « WPF_Nomducontrol ».
- La fonction PowerShell « Get-FormVariables » nous permet de lister l’ensemble des variables du script PowerShell.
- Ajoutons ce code à notre projet.
Pour dépanner, vous pouvez laisser l’exécution de la fonction, mais une fois le développement fini, vous pouvez commenter la fonction. Nous retrouvons les variables PowerShell associé aux types d’objets, dans notre cas, nous avons :
- WPF_MonBouton est associé à un Controls.Button qui a comme valeur Search
- WPF_MONCombobox est associé à un Controls.ComboBox qui ne contient aucun Items actuellement.
IV. Notre Application
Reprenons la fonction PowerShell pour retrouver la propriété « LastLogon » de nos utilisateurs au sein de notre Active Directory (même cas de figure que l'article avec Windows Forms).
function Get-ADUserLastLogon {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][ValidateScript({Get-ADUser $_})]$Identity=$null
)
# Récupérer la liste de tous les DC du domaine AD
$DCList = Get-ADDomainController -Filter * | Sort-Object Name | Select-Object Name
# Initialiser le LastLogon sur $null comme point de départ
$TargetUserLastLogon = $null
Foreach($DC in $DCList){
$DCName = $DC.Name
Try {
# Récupérer la valeur de l'attribut lastLogon à partir d'un DC (chaque DC tour à tour)
$LastLogonDC = Get-ADUser -Identity $Identity -Properties lastLogon -Server $DCName
# Convertir la valeur au format date/heure
$LastLogon = [Datetime]::FromFileTime($LastLogonDC.lastLogon)
# Si la valeur obtenue est plus récente que celle contenue dans $TargetUserLastLogon
# la variable est actualisée : ceci assure d'avoir le lastLogon le plus récent à la fin du traitement
If ($LastLogon -gt $TargetUserLastLogon)
{
$TargetUserLastLogon = $LastLogon
}
# Nettoyer la variable
Clear-Variable LastLogon
}
Catch {
Write-Host $_.Exception.Message -ForegroundColor Red
}
}
return $TargetUserLastLogon
}
A. Le code XAML
Pour cette application, voici le code XAML que nous pouvons écrire.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="480"
Height="200"
Title="IT Connect WPF">
<Grid>
<StackPanel HorizontalAlignment="Center" Margin="0,10,0,0" Orientation="Vertical">
<Label Name="Label" HorizontalAlignment="Center" Content="Active Directory" FontSize="20"/>
<GroupBox
Width="400"
Height="90"
Margin="0,10,0,0"
Header="Utilisateurs du domaine">
<StackPanel HorizontalAlignment="Center" Margin="0" Orientation="Vertical">
<StackPanel HorizontalAlignment="Center" Margin="0,10,0,0" Orientation="Horizontal">
<Button
Name="MonBouton"
Width="80"
Height="20"
Margin="0 10 0 0"
Content="Check"/>
<ComboBox Name="MonComboBox" Width="200" Height="20" Margin="5 10 0 0" />
</StackPanel>
<Label Name="Label2" Content="" Foreground="green"/>
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</Window>
Ce code nous permettra d’obtenir le rendu suivant :
B. Le code PowerShell
Maintenant que nous avons la partie graphique, attardons-nous sur la partie Scripting, je veux dire le code PowerShell.
# Ajout de l’assembly Presentation Framework
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null
# Récupération du chemin d’exécution du projet
$Global:pathPanel = split-path -parent $MyInvocation.MyCommand.Definition
# Fonction pour lire le fichier XAML
function LoadXaml ($filename) {
$XamlLoader = (New-Object System.Xml.XmlDocument)
$XamlLoader.Load($filename)
return $XamlLoader
}
$XamlMainWindow = LoadXaml("$Global:pathPanel\MonProjet.xaml")
$reader = (New-Object System.Xml.XmlNodeReader $XamlMainWindow)
$Form = [Windows.Markup.XamlReader]::Load($reader)
$XamlMainWindow.SelectNodes("//*[@Name]") | % {
try { Set-Variable -Name "$("WPF_"+$_.Name)" -Value $Form.FindName($_.Name) -ErrorAction Stop }
catch { throw }
}
Function Get-FormVariables {
if ($global:ReadmeDisplay -ne $true) { Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow; $global:ReadmeDisplay = $true }
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable *WPF*
}
# Pour afficher les variables, il vous suffit de décommenter la ligne suivante.
#Get-FormVariables
function Get-ADUserLastLogon {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][ValidateScript({ Get-ADUser $_ })]$Identity = $null
)
# Récupérer la liste de tous les DC du domaine AD
$DCList = Get-ADDomainController -Filter * | Sort-Object Name | Select-Object Name
# Initialiser le LastLogon sur $null comme point de départ
$TargetUserLastLogon = $null
# Date par défaut
$DefaultDate = [Datetime]'01/01/1601 01:00:00'
Foreach ($DC in $DCList) {
$DCName = $DC.Name
Try {
# Récupérer la valeur de l'attribut lastLogon à partir d'un DC (chaque DC tour à tour)
$LastLogonDC = Get-ADUser -Identity $Identity -Properties lastLogon -Server $DCName
# Convertir la valeur au format date/heure
$LastLogon = [Datetime]::FromFileTime($LastLogonDC.lastLogon)
# Si la valeur obtenue est plus récente que celle contenue dans $TargetUserLastLogon
# la variable est actualisée : ceci assure d'avoir le lastLogon le plus récent à la fin du traitement
If ($LastLogon -gt $TargetUserLastLogon) {
$TargetUserLastLogon = $LastLogon
}
# Nettoyer la variable
Clear-Variable LastLogon
}
Catch {
Write-Host $_.Exception.Message -ForegroundColor Red
}
}
if ($TargetUserLastLogon -eq $DefaultDate) {
return "Jamais"
}
else {
return $TargetUserLastLogon.ToString(" dd/MM/yyyy à HH:mm:ss ")
}
}
# Modification du texte du Label
$WPF_Label.Content = "Active Directory - LastLogon - $((Get-ADDomain).DNSRoot)"
Get-ADUser -Filter { Enabled -eq $true } | Select-Object samAccountName | Foreach {
[void]$WPF_MonComboBox.Items.Add($_.SamAccountName)
}
# Action sur clic
$WPF_MonBouton.Add_Click(
{
if ($WPF_MonComboBox.selectedItem -ne $null) {
$LastLogonOfUser = Get-ADUserLastLogon -Identity $($WPF_MonComboBox.selectedItem)
$WPF_Label2.Content = "Dernière connexion de $($WPF_MonComboBox.selectedItem) : $LastLogonOfUser"
}
}
)
$Form.ShowDialog() | Out-Null
Ce qui donne le résultat présenté ci-dessous, à savoir une application permettant d’obtenir la date de dernière connexion d’un utilisateur.
V. Les thèmes pour WPF
Avec nos applications WPF, nous avons plusieurs librairies ou DLL pour appliquer des thèmes à nos applications. Aujourd’hui, nous allons nous attarder sur deux thèmes.
- Mahapps Metro qui applique le design de Windows 10 (UWP) à nos application WPF avec la gestion des thèmes et des couleurs ainsi que des accents. Pour plus d’informations voici le site de Mahapps
- Material Design in XAML est lui orienté sur le design des applications Google pour le site web de Material Design In XAML
Les deux thèmes que nous allons appliquer à notre application on besoin de librairies additionnelles, que vous pouvez installer avec la commande dotnet. Voici un exemple :
A. Mahapps
Le thème Mahapps contient des icônes mais il est aussi possible d’avoir des icônes additionnelles. Pour notre projet, nous allons donc devoir ajouter un dossier dans lequel nous viendrons déposer les librairies ou DLL ainsi que leurs dépendances.
Dans mon nouveau dossier « Assembly », il y a ce contenu :
Quelles modifications au sein de notre XAML ? Tout d’abord, nous allons devoir définir une fenêtre Mahapps.
Ma balise "<Window></Window>" va devenir "<Controls:MetroWindow> </Controls:MetroWindow>".
Avec l’ajout des domaines de définition :
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
Et pour la gestion des thèmes blanc ou noir ainsi que les accents, un bloc "<Window.Resources></Window.Resources>" va être ajouté.
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Orange.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
Ajoutons maintenant une fonction qui va nous permettre de changer le thème de notre application. Pour cela nous allons rajouter un bouton dans la barre de l’application et forcément un peu de code PowerShell.
<Controls:MetroWindow.RightWindowCommands>
<Controls:WindowCommands>
<Button Name="Change" >
<iconPacks:PackIconOcticons Kind="Paintcan" Margin="0,5,0,0" ToolTip="Change le thème de l'application"/>
</Button>
</Controls:WindowCommands>
</Controls:MetroWindow.RightWindowCommands>
Ce code rajoute un control Button et on utilise ici une icône.
Mais, pour interagir avec, il nous faut évidemment un peu de code PowerShell, mon Button a comme nom « change ». J’ai donc une variable « $WPF_change ».
$WPF_change.Add_Click({
$Theme = [ControlzEx.Theming.ThemeManager]::Current.DetectTheme($form)
$my_theme = ($Theme.BaseColorScheme)
If($my_theme -eq "Light")
{
[ControlzEx.Theming.ThemeManager]::Current.ChangeThemeBaseColor($form,"Dark")
}
ElseIf($my_theme -eq "Dark")
{
[ControlzEx.Theming.ThemeManager]::Current.ChangeThemeBaseColor($form,"Light")
}
})
Voici le résultat obtenu :
Ça change tout et cela reste personnalisable.
La taille des objets étant plus grande, vous devrait peut-être effectuer des changements pour redimensionner certains objets.
Cliquez sur l'image ci-dessous pour voir l'animation.
IMPORTANT : lorsque que vous utilisez des thèmes pour lancer vos applications, vous devez les lancer avec la ligne « powershell.exe monprojet.ps1 », sinon vous aurais une erreur.
B. Material Design in XAML
Avec Material Design, nous besoins aussi de dépendances pour afficher le thème. Du coup, dans mon répertoire « assembly », j’ai les fichiers suivants :
Comme pour Mahapps, nous allons devoir ajouter ces librairies à notre projet pour les charger avec PowerShell.
Au final, nous obtenons le résultat suivant :
Cliquez sur l'image ci-dessous pour voir l'animation.
VI. Conclusion
En suivant ce tutoriel, vous devriez être en mesure de créer votre première application basée sur WPF et PowerShell ! Exercez-vous pour bien comprendre la mécanique et bien appréhender la syntaxe, et ainsi vous pencher sur la construction d’applications plus complexes.
Retrouvez tous les fichiers sources sur notre GitHub :