20/09/2024

PowerShell

PowerShell : comment créer une interface graphique avec Windows Forms ?

I. Présentation

Dans ce tutoriel, nous allons apprendre à utiliser Windows Forms avec PowerShell pour ajouter des éléments graphiques à notre script et créer une interface graphique. Ceci ouvre de nombreuses possibilités et va bien au-delà du script PowerShell en mode console.

Pour ce premier article sur le sujet, nous allons faire nos premiers pas avec Windows Forms dans un script PowerShell. Mais, avant d'aller plus loin, commençons par quelques mots sur Windows Forms et WPF...

Remarque : une interface graphique est couramment appelée GUI, sigle anglais signifiant "Graphical User Interface". 

II. PowerShell : Windows Forms et WPF

Pour développer des interfaces graphiques exploitables par PowerShell, il y a deux méthodes : utiliser "Windows Forms" ou "Windows Presentation Foundation" appelé "WPF". Dans les deux cas, PowerShell va s'appuyer sur des classes présentes dans le framework .NET.

Windows Forms représente la façon la plus simple et la plus rapide pour créer une première interface graphique et interagir avec l'utilisateur, de façon graphique, par l'intermédiaire d'un script PowerShell. Pour aller plus loin, et créer des interfaces plus complètes et plus modernes, il conviendra de s'appuyer sur WPF en utilisant Visual Studio pour faciliter la création et le positionnement d'éléments (un fichier XAML sera généré et exploité par PowerShell).

Avec Windows Forms, nous allons pouvoir :

  • Afficher des boites de dialogue à l'écran : information, erreur, avertissement, confirmation, etc.
  • Afficher une fenêtre de sélection d'un fichier ou d'un dossier
  • Créer une interface graphique pour l'utilisateur (appelée aussi "GUI") avec du texte, des boutons, etc.
  • Afficher une notification Windows sur l'écran
  • Etc.

III. Premiers pas avec Windows Forms

Nous verrons comment initialiser Windows Forms dans un script PowerShell, puis nous verrons comment créer nos premières interactions avec l'ajout de boites de dialogue, avant de terminer par la création d'une simple interface graphique.

A. Charger Windows Forms dans un script PowerShell

La première action à effectuer, c'est de charger Windows Forms dans le script PowerShell de façon à pouvoir accéder à ses différentes classes. Voici la ligne à intégrer au script en premier lieu :

# Appeler Windows Forms
Add-Type -AssemblyName System.Windows.Forms

B. Afficher un message dans une boite de dialogue

La classe "MessageBox" de Windows Forms permet d'afficher une boite de dialogue personnalisée à l'écran lorsqu'elle est utilisée avec la méthode "Show()". Cette méthode accepte plusieurs paramètres et nous l'utiliserons avec 4 paramètres différents conformément à ce qui décrit dans la documentation de Microsoft :

PositionDescriptionValeurs acceptées
1Texte de la boite de dialogueLe texte de votre choix
2Titre de la boite de dialogueLe texte de votre choix
3Type de bouton(s)0 - OK
1 - OK, Annuler
2 - Abandonner, Recommencer, Ignorer
3 - Oui, Non, Annuler
4 - Oui, Non
5 - Recommencer, Annuler
4Icône de la boite de dialogue0 - Aucun icône
32 - Interrogation
16 - Erreur
48 - Avertissement
64 - Information

La syntaxe, quant à elle, sera la suivante :

[System.Windows.Forms.MessageBox]::Show("<Message>", "<Titre>", <Type de bouton>, <Icône>)

Pour afficher notre première boite de dialogue, nous allons prendre l'exemple suivant : le script va compter le nombre de fichiers "PDF" dans le répertoire "C:\TEMP" de la machine locale et afficher le nombre de fichiers identifiés dans une MessageBox.

Ce qui donne ces deux lignes :

$FileCount = (Get-ChildItem -Path "C:\TEMP" -Filter "*.pdf" -Recurse).count
[System.Windows.Forms.MessageBox]::Show("Nombre de fichiers : $FileCount", "Script Démo", 0, 64 )

Voici le résultat obtenu à l'exécution du script :

PowerShell - Boite de dialogue

C. Afficher une fenêtre pour sélectionner un dossier

Nous allons faire évoluer le code précédent, de façon à rendre dynamique la sélection du dossier dans lequel rechercher les fichiers PDF. Pour que ce soit fait de façon interactive, nous allons ajouter un objet de type "FolderBrowserDialog" afin de demander à l'utilisateur de sélectionner un dossier.

Nous devons commencer par ajouter cette ligne pour créer un nouvel objet stocké dans la variable "$FolderSelect".

$FolderSelect = New-Object System.Windows.Forms.FolderBrowserDialog

Puis, nous allons afficher la boite de dialogue de sélection avec cette seconde ligne :

$FolderSelect.ShowDialog()

Ceci va afficher la fenêtre de sélection :

Après avoir sélectionné le dossier, nous devons cliquer sur "OK".

Pour accéder au chemin complet du dossier sélectionner, nous devons lire la propriété "SelectedPath" de notre objet. Cette information sera stockée dans une nouvelle variable :

$FolderSelectName = $FolderSelect.SelectedPath

Enfin, dans le code précédent, il ne reste plus qu'à remplacer le chemin statique par notre variable. Nous pouvons ajouter le chemin du dossier dans le message de la boite de dialogue (ou dans le titre). Ce qui donne :

$FileCount = (Get-ChildItem -Path $FolderSelectName -Filter "*.pdf" -Recurse).count
[System.Windows.Forms.MessageBox]::Show("Nombre de fichiers : $FileCount", "Script Démo - $FolderSelectName", 0, 64 )

Voici un exemple de résultat.

D. Afficher une fenêtre pour sélectionner un fichier

La sélection d'un ou plusieurs fichiers est également prise en charge, par l'intermédiaire d'un autre objet nommé "System.Windows.Forms.OpenFileDialog" et qui est personnalisable. Commençons par créer cet objet :

$FileSelect = New-Object System.Windows.Forms.OpenFileDialog

Puis, nous allons configurer plusieurs de ses propriétés pour le paramétrer. Tout d'abord, nous allons définir la propriété "InitialDirectory" qui sert à indiquer dans quel répertoire se positionne la boite de dialogue à son ouverture.

Voici deux exemples :

# Définir un chemin statique
$FileSelect.InitialDirectory = "C:\TEMP"
# Définir un chemin via GetFolderPath (chemin d'accès à un dossier spécial système, ici le "Bureau")
$FileSelect.InitialDirectory = [Environment]::GetFolderPath("Desktop")

Puis, nous allons définir la propriété "Filter" pour indiquer quels sont les types de fichiers autorisés. C'est facultatif, mais c'est bien de savoir comment faire lorsque le besoin de présente.

$FileSelect.Filter = <description du filtre>|<liste des extensions>

Voici deux exemples :

$FileSelect.Filter = "Fichiers de script (*.ps1;*.cmd;*.bat)|*.ps1;*.bat;*.cmd"
$FileSelect.Filter = "Fichiers images (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg"

Enfin, la propriété "Multiselect" (booléen) pour indiquer si nous pouvons sélectionner plusieurs fichiers.

$FileSelect.Multiselect = $false

Nous pourrions configurer d'autres propriétés si besoin. L'objet étant prêt, nous pouvons l'appeler pour l'afficher :

$File = $FileSelect.ShowDialog()

Voici le résultat :

PowerShell - Boite de dialogue charger un fichier

Il est intéressant de noter que la sélection du fichier renverra deux valeurs : "OK" lorsqu'un fichier a été sélectionné et "Cancel" lorsque l'opération a été annulée. Ceci signifie que l'on peut agir dans la suite du script uniquement si un fichier a été sélectionné.

if ( $File -eq "OK" ){
    Write-Host "Fichier sélectionné : $($FileSelect.FileName)"
}

Pour finir, voici le code complet :

# Charger Windows Forms
Add-Type -AssemblyName System.Windows.Forms

# Créer l'objet pour la boite de dialogue de sélection de fichiers
$FileSelect = New-Object System.Windows.Forms.OpenFileDialog

# Répertoire par défaut
$FileSelect.InitialDirectory = "C:\TEMP"
$FileSelect.InitialDirectory = [Environment]::GetFolderPath("Desktop")

# Filtre de fichiers
$FileSelect.Filter = "Fichiers de script (*.ps1;*.cmd;*.bat)|*.ps1;*.bat;*.cmd"
$FileSelect.Filter = "Fichiers images (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg"

# Sélection de plusieurs fichiers autorisés
$FileSelect.Multiselect = $false

# Afficher la boite de dialogue
$File = $FileSelect.ShowDialog()

# Tester le résultat
if ( $File -eq "OK" ){
    Write-Host "Fichier sélectionné : $($FileSelect.FileName)"
}

E. Créer un canva pour une GUI

Windows Forms est également capable de créer un canva, c'est-à-dire une fenêtre d'interface graphique au sein de laquelle nous pourrons venir positionner des éléments : texte, bouton, liste déroulante, liste de choix, etc... Afin de créer une application !

Nous devons créer un nouvel objet "System.Windows.Forms.Form" et définir ses propriétés, dont la largeur et la hauteur en pixels pour définir la taille de la fenêtre. L'exemple ci-dessous crée une fenêtre nommée "Recherche de fichiers" avec une largeur de 500 px et une hauteur de 200 px.

# Formulaire - Création du canva
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Recherche de fichiers"
$Form.Width = 500
$Form.Height = 200
$Form.AutoSize = $true

# Afficher la GUI
$Form.ShowDialog()

Chaque objet Windows Forms dispose d'un ensemble de propriétés et de méthodes que vous pouvez explorer avec Get-Member.

Le code ci-dessus donne l'interface suivante :

Ensuite, nous devons ajouter des éléments à notre interface. Chaque élément doit être déclaré et positionné en indiquant "ses coordonnées" en nombre de pixels, vis-à-vis du bord supérieur et du bord gauche de l'interface.

L'exemple ci-dessous écrit le texte "IT-Connect", tout simplement. La position est définie dans la propriété "$Label.Location" de l'objet "System.Windows.Forms.Label". La propriété "Visible" est également intéressante, car elle sert à afficher ou masquer un élément sur le canva. Ceci pourra s'avérer utile pour afficher un élément en fonction d'une action telle que le clic sur un bouton.

# Texte "IT-Connect"
$Label = New-Object System.Windows.Forms.Label
$Label.Text = "IT-Connect"
$Label.Location = New-Object System.Drawing.Point(10,10)
$Label.AutoSize = $true
$Label.Visible = $true
$Form.Controls.Add($Label)

Voici le résultat :

Il est important de noter que la ligne permettant d'afficher la GUI doit forcément être présente à la fin du script, sinon des objets ne seront pas pris en compte :

# Afficher la GUI
$Form.ShowDialog()

F. Créer sa première GUI en PowerShell

Reprenons notre idée initiale : compter le nombre de fichiers correspondants à un type d'extension présent au sein d'un répertoire. Pour que ce soit plus interactif, nous allons rendre cela possible au travers d'une interface graphique. Elle permettra de sélectionner le dossier dans lequel effectuer la recherche et le type d'extensions à rechercher (en proposant une liste dynamique correspondante à la liste des extensions identifiées dans le dossier sélectionné).

La création d'une application avec une interface graphique implique la gestion des événements et des actions. En effet, nous devons gérer l'état de chaque élément (sa valeur, affiché, masqué, etc.) et définir les actions effectuées par chaque bouton.

Nous devons commencer par créer l'ensemble des objets (boutons, textes, etc.), les configurer et les positionner sur le Canva. Certains éléments seront invisibles, car conditionné à la première action à effectuer par l'utilisateur : la sélection du dossier dans lequel rechercher.

Ce qui donne ceci :

# Formulaire - Création du canva
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Recherche de fichiers"
$Form.Width = 500
$Form.Height = 200
$Form.AutoSize = $true

# Bouton pour sélectionner un dossier
$ButtonSelect = New-Object System.Windows.Forms.Button
$ButtonSelect.Location = New-Object System.Drawing.Size(10,10)
$ButtonSelect.Size = New-Object System.Drawing.Size(150,25)
$ButtonSelect.Text = "Sélectionner un dossier"
$Form.Controls.Add($ButtonSelect)

# Bouton pour valider la recherche
$ButtonValidate = New-Object System.Windows.Forms.Button
$ButtonValidate.Location = New-Object System.Drawing.Size(10,90)
$ButtonValidate.Size = New-Object System.Drawing.Size(100,25)
$ButtonValidate.Text = "Valider"
$ButtonValidate.Visible = $false
$Form.Controls.Add($ButtonValidate)

# Texte : Type d'extension
$LabelTypeExt = New-Object System.Windows.Forms.Label
$LabelTypeExt.Text = "Type d'extension"
$LabelTypeExt.Location = New-Object System.Drawing.Point(10,65)
$LabelTypeExt.AutoSize = $true
$LabelTypeExt.Visible = $false
$Form.Controls.Add($LabelTypeExt)

# Liste déroulante pour choisir une extension
$ComboBoxExtList = New-Object System.Windows.Forms.ComboBox
$ComboBoxExtList.Width = 150
$ComboBoxExtList.Visible = $false
$ComboBoxExtList.Location  = New-Object System.Drawing.Point(110,60)
$Form.Controls.Add($ComboBoxExtList)

Ensuite, nous devons utiliser la méthode "Add_Click()" sur nos boutons pour déclencher une action lors d'un clic.

  • Lors d'un clic sur le bouton "Sélectionner un dossier", l'application doit proposer à l'utilisateur de sélectionner un dossier. Une fois le dossier sélectionné, nous récupérons la liste de toutes les extensions identifiées dans le répertoire pour alimenter la liste déroulante.
  • Lors d'un clic sur le bouton "Valider", PowerShell effectue une recherche pour compter le nombre de fichiers correspondants à l'extension préalablement sélectionnée par l'utilisateur.

Notre code va contenir deux déclarations de la méthode "Add_Click()", comme ceci :

# Action : clic sur le bouton "Sélectionner un dossier"
$ButtonSelect.Add_Click(
{
    $FolderSelect = New-Object System.Windows.Forms.FolderBrowserDialog
    $FolderSelect.ShowDialog()

    $global:FolderSelectName = $FolderSelect.SelectedPath

    if($FolderSelectName -ne "")
    {
        $Form.Text = "Recherche de fichiers dans : $FolderSelectName"
        $LabelTypeExt.Visible = $true
        $ComboBoxExtList.Visible = $true
        $ButtonValidate.Visible = $true
        $ComboBoxExtList.AutoCompleteSource
        $ComboBoxExtList.Items.Clear()

        Get-ChildItem $FolderSelectName -Recurse | Where-Object {!$_.PSIsContainer} | Select-Object Extension -Unique | Foreach{

            $ComboBoxExtList.Items.Add($_.Extension);

        }
    }
}

)

# Action : clic sur le bouton "Valider"
$ButtonValidate.Add_Click(

{
    if($ComboBoxExtList.selectedItem -ne ""){
        $FileCount = (Get-ChildItem -Path $FolderSelectName -Filter "*$($ComboBoxExtList.selectedItem)" -Recurse).count
        [System.Windows.Forms.MessageBox]::Show("Nombre de fichiers $($ComboBoxExtList.selectedItem) : $FileCount", "Script Démo - $FolderSelectName", 0, 64 )
    }
}

)

J'attire votre attention sur la ligne ci-dessous où nous jouons sur la portée de la variable, sinon sa valeur n'est pas accessible au reste du script, ce qui pose problème.

$global:FolderSelectName = $FolderSelect.SelectedPath

Sans oublier, à la fin, la ligne pour afficher la GUI :

# Afficher la GUI
$Form.ShowDialog()

Lorsque le script PowerShell est exécuté, nous obtenons le résultat suivant. Ce GIF permet de voir l'application en action.

Cet exemple relativement simple permet de se familiariser avec les concepts de base de la création d'une interface graphique en PowerShell, via Windows Forms. Ceci ouvre de nouvelles possibilités et nous force à penser différemment, car les interactions avec un script ou une GUI sont différentes.

G. Interroger l'Active Directory

En couplant l'utilisation de PowerShell à Windows Forms, il est possible de développer des scripts interactifs très pratiques et liés à différents services, dont l'Active Directory. L'exemple ci-dessous est un bon exemple, tout en étant simple : le script permet de consulter la date de dernière connexion d'un utilisateur Active Directory. La liste déroulante est alimentée de façon dynamique grâce au résultat de la commande "Get-ADUser".

Voici le code complet correspondant à cet exemple :

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

}

# Appeler Windows Forms
Add-Type -AssemblyName System.Windows.Forms

# Formulaire - Création du canva
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Active Directory - LastLogon - $((Get-ADDomain).DNSRoot)"
$Form.Width = 500
$Form.Height = 150
$Form.AutoSize = $true

# Bouton pour vérifier le LastLogon de l'utilisateur
$ButtonCheck = New-Object System.Windows.Forms.Button
$ButtonCheck.Location = New-Object System.Drawing.Size(10,80)
$ButtonCheck.Size = New-Object System.Drawing.Size(100,25)
$ButtonCheck.Text = "Vérifier"
$ButtonCheck.Visible = $true
$Form.Controls.Add($ButtonCheck)

# Texte : Sélectionner un utilisateur
$LabelSelectUser = New-Object System.Windows.Forms.Label
$LabelSelectUser.Text = "Sélectionner un utilisateur"
$LabelSelectUser.Location = New-Object System.Drawing.Point(10,12)
$LabelSelectUser.AutoSize = $true
$LabelSelectUser.Visible = $true
$Form.Controls.Add($LabelSelectUser)

# Liste déroulante pour choisir un utilisateur AD
$ComboBoxUserList = New-Object System.Windows.Forms.ComboBox
$ComboBoxUserList.Width = 150
$ComboBoxUserList.Visible = $true
$ComboBoxUserList.Location  = New-Object System.Drawing.Point(150,10)
$Form.Controls.Add($ComboBoxUserList)

# Texte avec le résultat : LastLogon et nom de l'utilisateur
$Label = New-Object System.Windows.Forms.Label
$Label.Text = ""
$Label.Location = New-Object System.Drawing.Point(10,40)
$Label.AutoSize = $true
$Label.Visible = $true
$Label.ForeColor = "green"
$Form.Controls.Add($Label)

# Ajouter des valeurs à la liste déroulante
Get-ADUser -Filter {Enabled -eq $true} | Select-Object samAccountName | Foreach{

    $ComboBoxUserList.Items.Add($_.SamAccountName);

}

# Action : clic sur le bouton "Vérifier"
$ButtonCheck.Add_Click(

{
    if($ComboBoxUserList.selectedItem -ne $null){
        $LastLogonOfUser = Get-ADUserLastLogon -Identity $($ComboBoxUserList.selectedItem)
        $Label.Text = "Dernière connexion de $($ComboBoxUserList.selectedItem) : $LastLogonOfUser"
    }else{
        [System.Windows.Forms.MessageBox]::Show("Vous devez sélectionner un utilisateur !", "Erreur - $((Get-ADDomain).DNSRoot)", 0, 16 )
    }
}

)

# Afficher la GUI
$Form.ShowDialog()

Pour approfondir ce sujet, vous pouvez lire notre cours dédié à l'administration de PowerShell avec l'Active Directory :

IV. Conclusion

Suite à la lecture de cet article, vous devriez être en mesure de créer votre première interface graphique dans un script PowerShell !

Commencez par des choses simples avant de vouloir aller plus loin, et par la suite, envisagez de vous intéresser à WPF (qui pourra faire l'objet de futurs articles). Windows Forms est un très bon moyen de commencer sans trop forcer.

author avatar
Florian BURNEL Co-founder of IT-Connect
Ingénieur système et réseau, cofondateur d'IT-Connect et Microsoft MVP "Cloud and Datacenter Management". Je souhaite partager mon expérience et mes découvertes au travers de mes articles. Généraliste avec une attirance particulière pour les solutions Microsoft et le scripting. Bonne lecture.
Partagez cet article Partager sur Twitter Partager sur Facebook Partager sur Linkedin Envoyer par mail

5 commentaires sur “PowerShell : comment créer une interface graphique avec Windows Forms ?

  • Super article !
    Il manque juste dans le script AD – LastLogon le chargement de Windows Form…

    Add-Type -AssemblyName System.Windows.Forms

    Répondre
  • Une toute petite amélioration de la fonction dans le cas ou l’utilisateur ne s’est jamais connecté (la date est alors 01/01/1601 01:00:00) et avec un affichage au format « Jour/Mois/Année à Heure:Minute:Seconde » pour une meilleure lisibilité :
    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 »)
    }

    }

    Répondre
  • Bonjour à toute l’équipe,
    Que signifie le code couleur dans la syntaxe ? Le s mots en rouge, en bleu ?

    Et deuxième question :
    La deuxième ligne de code ne passe pas pour moi. Il y une erreur avec une virgule ou une parenthèse.

    PS C:\Users\admin.max> [System.Windows.Forms.MessageBox]::Show(«  », «  », , )
    ParserError:
    Line |
    1 | … ystem.Windows.Forms.MessageBox]::Show(«  », «  », [System.Windows.Forms.MessageBox]::Show(«  » «  », , )
    ParserError:
    Line |
    1 | [System.Windows.Forms.MessageBox]::Show(«  » «  », <Type …
    | ~
    | Missing ' )' in method call.

    Répondre
  • Excellente introduction,
    J’aurais aimé avoir cet article lorsque j’ai fais ma propre application (simple application qui permet de choisir une source audio pour une salle multimédia).

    Je le garde sous la main.

    Répondre

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.