Powershell : création d’interfaces graphiques – troisième partie

Suite (et peut-être fin ?) des deux premiers articles concernant la création d’interfaces graphiques en Powershell ; après avoir vu les contrôles de base et les EventHandlers dans la première partie puis l’activation de contrôles ainsi que leur organisation au niveau spatial dans la seconde partie, je vais essayer d’aborder différemment les choses dans ce billet puisque nous allons développer ensemble une petite application récupérant les informations concernant les processeurs d’une station grâce à WMI et placer ces informations récupérées dans des contrôles. Cela est en quelque sorte une sorte de résumé ou de super-tutoriel par rapport à ce qui a été abordé précédemment, avec tout de même du nouveau contenu en termes de contrôles comme le DataGridView ou d’EventHandlers.

Tout d’abord, nous allons définir l’interface : une seule fenêtre, une TextBox pour saisir le nom de la station que nous souhaitons analyser, un Label pour indiquer le statut de la requête, 2 boutons (un pour lancer la requête, un deuxième pour effacer le tableau) et un DataGridView pour y renseigner les informations.

La Form :

$Form = New-Object System.Windows.Forms.Form
$Form.ClientSize = ‘400,450’
$Form.Text = « Mon UI en PS ep. III »
$Form.FormBorderStyle = ‘Fixed3D’
$Form.MaximizeBox = $false

Le Label de statut :

$LabelStatus = New-Object System.Windows.Forms.Label
$LabelStatus.Location = New-Object System.Drawing.Point(20,20)
$LabelStatus.Text = « Saisir un nom de station. »
$Form.controls.Add($LabelStatus)

La TextBox :

$TextBoxStation = New-Object System.Windows.Forms.TextBox
$TextBoxStation.Location = New-Object System.Drawing.Point(20,50)
$TextBoxStation.Width = 100
$TextBoxStation.TabIndex = 1
$Form.controls.Add($TextBoxStation)

Les Button pour déclencher la requête et vider le tableau :

$BoutonGetWMI = New-Object System.Windows.Forms.Button
$BoutonGetWMI.Location = New-Object System.Drawing.Point(150,50)
$BoutonGetWMI.Text = « Afficher les infos »
$BoutonGetWMI.Width = 100
$BoutonGetWMI.TabIndex = 2
$BoutonGetWMI.Enabled = $false
$BoutonClearTab = New-Object System.Windows.Forms.Button
$BoutonClearTab.Location = New-Object System.Drawing.Point(280,50)
$BoutonClearTab.Text = « Effacer le tableau »
$BoutonClearTab.Width = 100
$BoutonClearTab.TabIndex = 3
$BoutonClearTab.Enabled = $false
$Form.controls.AddRange(@($BoutonGetWMI,$BoutonClearTab))

Et enfin, le tableau, qui est représenté par l’objet DataGridView :

$DataGridViewCPU = New-Object System.Windows.Forms.DataGridView
$DataGridViewCPU.Location = New-Object System.Drawing.Point(20,80)
$DataGridViewCPU.Size = ‘360,290’
$DataGridViewCPU.ReadOnly = $true
$DataGridViewCPU.RowHeadersVisible = $false
$DataGridViewCPU.ColumnCount = 3
$DataGridViewCPU.Columns[0].Name = « CPU # »
$DataGridViewCPU.Columns[0].Width = 55
$DataGridViewCPU.Columns[1].Name = « Reference »
$DataGridViewCPU.Columns[1].Width = 140
$DataGridViewCPU.Columns[2].Name = « Freq. en MHz »
$DataGridViewCPU.Columns[2].Width = 140
$Form.controls.Add($DataGridViewCPU)

Les propriétés TabIndex permettent de définir l’ordre dans lequel les contrôles s’activent sur pression de la touche de tabulation. Avec ce code, l’interface est rendue comme telle :

L’interface de base une fois le code de l’UI terminé.
Concernant le DataGridView, plusieurs explications :
  • Cet objet fonctionne comme une collection d’autres objets : DataGridViewRow pour les lignes et DataGridViewColumn pour les colonnes.
  • La propriété ReadOnly permet d’éviter que l’utilisateur saisisse des informations dans les cases.
  • En fonction de ce que l’on souhaite afficher et de la liberté offerte à l’utilisateur, il est possible de bloquer le redimensionnement des colonnes, le tri, etc. Je vous renvoie vers la documentation Microsoft dont les liens sont en fin d’article pour plus d’informations.
Maintenant, il s’agit de coder les EventHandlers ; tout d’abord sur la TextBox, afin d’éviter que l’on lance la requête WMI si la TextBox est vide. Cet EventHandler se déclenche dès que son contenu est modifié : si le texte est vide, le bouton est grisé.

$TextBoxStation.Add_TextChanged({
if ($TextBoxStation.Text -eq «  ») { $BoutonGetWMI.Enabled = $false }
else { $BoutonGetWMI.Enabled = $true }

L’EventHandler pour vider le DataGridView. On efface toutes les lignes de celui-ci.

$BoutonClearTab.Add_Click({
$DataGridViewCPU.Rows.Clear()
$TextBoxStation.Enabled = $true
$TextBoxStation.Text = «  »
   $BoutonClearTab.Enabled = $false })

Et enfin, l’EventHandler sur le bouton de récupération des informations ; c’est ici que l’essentiel du « vrai » code sera placé.

$BoutonGetWMI.Add_Click({
$CPUArray = Get-WmiObject win32_Processor -computername $TextBoxStation.Text
if ($?) {
foreach ($CPU in $CPUArray){
$DataGridViewCPU.Rows.Insert(0,$CPU.DeviceID,$CPU.Name,$CPU.MaxClockSpeed) }
$LabelStatus.Text = « Informations obtenues. »
$BoutonGetWMI.Enabled = $false
$BoutonClearTab.Enabled = $true
$TextBoxStation.Enabled = $false

}
else { $LabelStatus.Text = « Erreur. » }
})

Plusieurs choses concernant ces snippets :
  • En bleu, le code pour désactiver ou activer les boutons en fonction des besoins ; en faisant ceci, je m’assure que l’utilisateur ne peut pas lancer une requête et donc repeupler le tableau d’informations, je l’empêche également de modifier la saisie dans la TextBox. Ces contrôles sont réactivés lorsque le tableau est effacé.
  • En vert, cette condition permet de vérifier si la dernière instruction Powershell a renvoyé une erreur. Cela permet donc de réellement peupler le DataGridView si les objets ont bien été retournés ($? = $true) par la requête WMI ou de modifier le Label en conséquence sinon ($? = $false).
  • En orange, l’instruction permettant de remplir le DataGridView : bien qu’il soit possible d’ajouter les lignes une par une, nous allons dans notre cas les insérer. Afin de pouvoir ajouter un objet DataGridViewRow dans le DataGridView, il faut spécifier l’index ainsi que des valeurs pour les colonnes qui ont été déclarées. Avec cette commande, nous insérons donc tout en haut du tableau (index 0) les valeurs qui nous intéressent dans l’objet retourné par la requête WMI.
La requête n’a pas fonctionné.
La requête a fonctionné.

Par la suite, il est possible d’exploiter le contenu des cellules du DataGridView en appelant simplement les objets dont il est composé. Par exemple, on peut passer une boucle Foreach si l’on souhaite compter le nombre de CPU dont la fréquence est supérieure à 3000 MHz. En reprenant notre DataGridView, la troisième cellule de la ligne est celle de la fréquence, il faut donc analyser son contenu. Attention, les index débutent toujours par 0 et c’est un objet DataGridViewCell que nous allons appeler, ce qui nous intéresse pour la comparaison est sa propriété Value.

$ProcHauteFreq = 0
foreach ($Row in $DataGridViewCPU.Rows) {
if ($Row.Cells[2].Value -gt 3000) { $ProcHauteFreq++ }
}

On peut aussi par exemple exploiter le contenu simple d’une seule ou plusieurs cases. Dans cet exemple, le contenu de la case sélectionnée par l’utilisateur ira remplir le Label. Si plusieurs cellules sont sélectionnées, un tableau de DataGridViewCell sera retourné. Afin d’éviter une multiple sélection par l’utilisateur, on peut affecter la propriété Multiselect à False de cette manière :

$DataGridViewCPU.Multiselect = $False

$LabelStatus.Text = $DataGridViewCPU.SelectedCells.Value

Liens

Développement d’une UI en Powershell : première partiedeuxième partie
Documentation et références des classes : DataGridViewDataGridViewRowDataGridViewColumn – DataGridViewCell

Quelques billes pour crypter des stations avec BitLocker

Je travaille en ce moment sur BitLocker et après quelques jours de recherches, de tests et d’usure intensive de disques durs, je suis enfin arrivé à un résultat satisfaisant pour obtenir une GPO définissant des paramètres de sécurité rassurants. Je n’ai pas pour prétention d’avoir trouvé la formule magique vous permettant de crypter dès demain en quelques heures 25,000 stations mais ces quelques notes que je vais partager peuvent constituer une bonne base de travail ou d’appréhension de la technologie. Dans mon exemple, je pars sur des postes clients en Windows 10 équipés d’une puce TPM et avec un AD en niveau 2008 R2.

Il s’agit donc de définir proprement la GPO qui sera récupérée par les postes éligibles et ensuite de lancer le cryptage. Tous les paramètres se retrouvent dans l’arborescence suivante : Computer Configuration > Administrative Templates > Windows Components > BitLocker Drive Encryption. Il y a 3 politiques à définir : l’une concernant le disque (au sens logique du terme), la deuxième pour les disques internes et la troisième pour les disques externes.

Concernant les disques hébergeant l’OS :

  • Require additional authentication at startup propose plusieurs réglages et c’est ici que l’on définit ce qui est nécessaire pour continuer le boot du système : la puce TPM simple, un code PIN et la puce TPM, une clef USB avec la puce TPM avec informations de démarrage, ou les 3 à la fois. Attention, un seul de ces 4 paramètres peut être en « requis » (paramètre strict).
  • Si la solution choisie est celle du PIN, il est possible de modifier la longueur minimale de celui-ci dans la très claire propriété Configure minimum PIN length at startup. Par défaut, il ne s’agit que de caractères numériques mais en activant le paramètre dans Allow enhanced PINs for startup, alors les caractères alphanumériques seront autorisés.
  • Dans Enforce Drive encryption type on operating system drive on peut choisir ce qui est crypté sur le disque : l’espace utilisé ou tout le disque. Ce paramètre existe pour les disques locaux non-systèmes.
  • Choose how BitLocker-protected operating system drives can be recovered est un paramètre intéressant car c’est ici que l’on va définir comment récupérer la boulette d’un utilisateur ayant perdu son PIN ou sa clef de déverrouillage. Cela peut se faire soit par un mot de passe de 48 caractères généré aléatoirement lors du cryptage du disque, soit par une clef 256-bits (ou les 2). Ce qui est intéressant ici, c’est que l’on peut stocker dans AD DS ces informations, ce qui fait que l’utilisateur n’a pas à les noter (sous-entendu à écrire la clef de récupération sur un post-it collé sous le laptop ou sur une feuille volante dans la sacoche) et d’un point de vue administrateur, cela permet de tout avoir centré dans un dispositif déjà existant. Afin de stocker ces informations dans l’AD, il y a plusieurs pré-requis, je vous renvoie à cette documentation Microsoft pour plus d’informations. Pour plus de robustesse, il est possible de rendre impossible l’encryption du disque si la sauvegarde des moyens de récupération ne peut pas être effectuée dans l’AD.

Concernant les disques locaux non-système :

  • En fonction de la sécurité à apporter à la station, il est possible de refuser toute écriture sur un disque qui n’est pas crypté par BitLocker dans Deny write access to fixed drives not protected by BitLocker.
  • Les disques non-système ne pouvant évidemment pas demander un PIN, ils peuvent être protégés par un mot de passe. Avec Configure use of passwords for fixed data drives, on définit une politique par rapport à ces mots de passe : longueur et complexité ; la complexité adoptée pour le mot de passe du disque étant la même que celle pour les mots de passe du domaine. Attention à l’éventuel côté pénible d’avoir à se souvenir de « 5847562369 » comme PIN de démarrage et de « @nt1c0n$TitUt10n€II3mEn|- » pour déverrouiller le disque des données…
  • Tout comme les disques système, les moyens de récupération des disques locaux peuvent être stockés dans l’ActiveDirectory grâce aux mêmes réglages offerts dans Choose how BitLocker-protected operating system drives can be recovered.

Concernant les disques amovibles :

  • Une liberté peut être donnée à l’utilisateur de crypter ou non une clef USB ou un disque externe grâce à Control use of BitLocker on removable drives.
  • Et l’on retrouve les mêmes réglages que pour les disques locaux usuelsConfigure use of passwords for fixed data drives et Choose how BitLocker-protected operating system drives can be recovered.

De manière générale :

  • La sauvegarde des mots de passe de récupération et des clefs 256-bits de décryptage s’active grâce au paramètre Store BitLocker recovery information in Active Directory Domain Services. En activant l’option et en demandant de stocker ces informations dans AD DS, la sécurité est augmentée et le travail des administrateurs facilité. Il est également possible de stocker la clef dans un emplacement réseau dans Choose default folder for recovery password.
  • En fonction de l’OS cible, divers algorithmes de cryptages sont disponibles. Avec Windows 10, on peut choisir XTS-AES 256-bit qui semble être le plus sécurisé, avec un inconvénient : cet algorithme n’est pas pris en charge sur les versions inférieures. Les disques internes des stations n’ayant pas pour but d’être démontées et changées de station dans un modèle et avec un OS différent tous les jours, cela ne pose pas de problème en soi ; concernant les périphériques amovibles, la question se pose car il peut être branché sur divers appareils exécutant des OS différents : on peut donc se rabattre sur un algorithme AES-CBC.

Ensuite, côté poste de travail, c’est l’outil manage-bde qui permet d’appliquer le cryptage BitLocker sur les stations ; à noter qu’il faut préalablement que BitLocker ait été activé dans le Panneau de configuration sans quoi manage-bde sera incapable de voir l’intégralité des disques. Il est important de saisir que manage-bde crypte et possède des fonctionnalités basés sur la GPO définie plus haut. Vous ne pourrez par exemple pas crypter la station sans PIN ou ne pas obtenir de mot de passe de récupération si ils sont requis ou crypter avec un algorithme différent de celui renvoyé par la GPO.

Par exemple, si je souhaite crypter mon disque système et qu’un code PIN ainsi que mot de passe de récupération sont nécessaires, cela va se dérouler ainsi dans un cmd en administrateur :

manage-bde -on c: -tpmandpin -rp

Le PIN est demandé après la validation de l’instruction. Dans le cadre d’un disque non-système :

manage-bde -on d: -rp -password

Le mot de passe est demandé après l’envoi de la commande. Bien évidemment, l’outil manage-bde offre d’autres possibilités avec d’autres arguments, je vous renvoie à la documentation de MS à ce sujet.

A noter que l’utilisateur peut ensuite changer son PIN ou le mot de passe de déverrouillage de son disque. D’autres paramètres sont accessibles si l’utilisateur est administrateur du poste.

Une fois le cryptage commencé, on peut retrouver dans l’AD sur les propriétés d’un objet Ordinateur les informations de récupérations définies dans la GPO grâce à l’onglet BitLocker Recovery.

Oui, j’ai réalisé de nombreux tests sur mon poste pilote.

Cet onglet est visible une fois que les outils de management BitLocker ont été déployés sur le contrôleur de domaine (ou sur la machine hébergeant les outils de management).

Powershell : création d’interfaces graphiques – deuxième partie

Après une première partie traitant des bases de la conception d’une UI en Powershell, voici un deuxième article pour aller un peu plus loin. Je vais aborder dans cet article les points suivants :

  • L’ajout d’un menu en haut de la fenêtre grâce au contrôle MenuStrip
  • L’ajout de contrôles GroupBox pour mieux organiser les contrôles
  • L’activation ou désactivation de contrôles
  • L’ajout de bulles d’info au passage de la souris sur les contrôles
Si vous n’avez pas lu le premier article, je vous invite à le faire car je ne vais pas revenir sur les points qui ont été abordés précédemment.

Création d’une barre de menus

Tout d’abord, il s’agit de définir la fenêtre :

$Form = New-Object System.Windows.Forms.Form
$Form.ClientSize = ‘500,500’
$Form.Text = « Mon UI en PS ep. II »
$Form.FormBorderStyle = ‘Fixed3D’
$Form.MaximizeBox = $false

FormBorderStyle réglé sur Fixed3D permet d’éviter à l’utilisateur de prendre le coin de la fenêtre et l’étendre ; ce qui fausserait tout le design de l’interface puisque nous nous basons sur des coordonnées par rapport à une taille de fenêtre définie dans le code. MaximizeBox sur False permet de désactiver le bouton d’agrandissement de la fenêtre. Avec ces simples paramètres, nous barrons déjà la possibilité pour l’utilisateur de nuire à sa propre expérience par deux fois.
Ensuite, nous allons nous servir du contrôle MenuStrip pour définir un menu en haut de notre fenêtre.

$Menu = New-Object System.Windows.Forms.MenuStrip
$Menu.Location = New-Object System.Drawing.Point(0,0)
$Menu.ShowItemToolTips = $True

Le menu est bien ajouté et visible :
Notre fenêtre vide avec son menu en haut. Ne reste qu’à ajouter des éléments sur ce menu.
Pour ajouter des sections dans ce menu, il faut utiliser l’objet ToolStripMenuItem, qu’on va instancier comme n’importe quel autre objet. Avec ce code, je vais donc ajouter une section « Fichier » qui aura un élément « Quitter », et une section « A propos » qui n’aura aucun élément.

$MenuFile = New-Object System.Windows.Forms.ToolStripMenuItem
$MenuFile.Text = « &Fichier »
$MenuAbout = New-Object System.Windows.Forms.ToolStripMenuItem
$MenuAbout.Text = « &A propos »
$MenuFileQuit = New-Object System.Windows.Forms.ToolStripMenuItem
$MenuFileQuit.Text = « &Quitter »

En ajoutant le symbole & au début de chaque titre, je permets au contrôle de réagir par raccourci clavier. Un ALT + F ira donc ouvrir la section « Fichier ». Une fois ces trois objets créés, il faut indiquer que je souhaite avoir mon « Quitter » dans la section « Fichier » ; c’est réalisable avec la méthode DropDownItems.Add :

$MenuFile.DropDownItems.Add($MenuFileQuit)

Et enfin, nous allons rattacher les deux ToolStripMenuItem à notre MenuStrip :

$Menu.Items.AddRange(@($MenuFile,$MenuAbout))

Voici le résultat :
Nous allons rajouter une info-bulle et ajouter un EventHandler sur « Quitter ».

$MenuFileQuit.ToolTipText = « Infobulle d’aide »

$MenuFileQuit.Add_Click({
$Form.Close()
})

Nous obtenons ceci :

Et lorsque nous cliquons sur « Quitter », la fenêtre et le script se terminent.

Mieux organiser ses contrôles avec des GroupBox

Le contrôle GroupBox permet d’encadrer simplement plusieurs contrôles à des fins visuelles. Je vais ajouter à la Form quelques contrôles Label, Button et deux GroupBox pour les séparer. Je vais donc créer un Label qui va afficher le nom de l’utilisateur sur clic du bouton associé et son pendant pour le nom de la machine exécutant le script ; en rangeant tout cela dans des GroupBox. Afin de les définir proprement, il faut spécifier la hauteur, largeur et la légende. A noter qu’il est important de définir les GroupBox après les contrôles que l’on souhaite placer dedans, sans quoi la GroupBox les masquera au lieu de les intégrer.

$LabelLogUser = New-Object System.Windows.Forms.Label
$LabelLogUser.Location = New-Object System.Drawing.Point(40,70)
$LabelLogUser.AutoSize = $true 

$LabelCompName = New-Object System.Windows.Forms.Label
$LabelCompName.Location = New-Object System.Drawing.Point(300,70)
$LabelCompName.AutoSize = $true 

$BoutonGetUser = New-Object System.Windows.Forms.Button
$BoutonGetUser.Location = New-Object System.Drawing.Point(40,150)
$BoutonGetUser.Width = 125
$BoutonGetUser.Text = « Nom user » 

$BoutonGetComp = New-Object System.Windows.Forms.Button
$BoutonGetComp.Location = New-Object System.Drawing.Point(300,150)
$BoutonGetComp.Width = 125
$BoutonGetComp.Text = « Nom station » 

$Form.controls.AddRange(@($LabelLogUser,$LabelCompName))
$Form.controls.AddRange(@($BoutonGetComp,$BoutonGetUser))

$GroupBoxUser = New-Object System.Windows.Forms.GroupBox
$GroupBoxUser.Location = New-Object System.Drawing.Point(20,50)
$GroupBoxUser.Width = 180
$GroupBoxUser.Height = 220
$GroupBoxUser.Text = « User » 

$GroupBoxComp = New-Object System.Windows.Forms.GroupBox
$GroupBoxComp.Location = New-Object System.Drawing.Point(250,50)
$GroupBoxComp.Width = 180
$GroupBoxComp.Height = 220
$GroupBoxComp.Text = « Station »

$Form.controls.AddRange(@($GroupBoxUser,$GroupBoxComp))

Exécutons :

N’ayant pas ajouté de propriété Text à mes Label, ils n’apparaissent donc pas pour l’instant, mais en ajoutant des EventHandlers aux boutons, ils apparaîtront. Je vais coder 2 handlers sur les boutons :

$BoutonGetUser.Add_Click({
$LabelLogUser.Text = $env:username
})

$BoutonGetComp.Add_Click({
$LabelCompName.Text = hostname
})

Et mon Label apparaît suite au clic sur le bouton pour récupérer le nom de la station :

Activation et désactivation de contrôles

Avec ces deux boutons, nous allons pouvoir voir la propriété Enabled qui existe sur beaucoup de contrôles : elle permet de rendre (in)-utilisable ledit contrôle par l’utilisateur. Dans le cadre d’un bouton, il sera grisé. Je vais changer le texte des boutons puis modifier les EventHandlers liés aux boutons pour qu’un clic sur le premier le désactive en activant le second et réciproquement. Par défaut, la propriété Enabled d’un contrôle est à True, ce qui signifie qu’à moins de la passer à False à un moment dans votre code, ce contrôle sera toujours actif.

$BoutonGetUser.Add_Click({
$BoutonGetUser.Enabled = $false
$BoutonGetComp.Enabled = $true
$BoutonGetUser.Text = « Inactif »
$BoutonGetComp.Text = « Actif »
})

$BoutonGetComp.Add_Click({
$BoutonGetComp.Enabled = $false
$BoutonGetUser.Enabled = $true
$BoutonGetComp.Text = « Inactif »
$BoutonGetUser.Text = « Actif »
})

Voyons le résultat :

Ajout de tooltips au passage de la souris

Certains contrôles comme le MenuStrip évoqué plus haut gèrent « nativement » les bulles d’information. D’autres ont besoin d’un objet, simplement nommé ToolTip. L’ajout d’une info-bulle sur un contrôle se fait en deux phases : la première consiste en la création de l’objet et la deuxième du rattachement de cet objet au contrôle en question. Ce snippet va créer deux objets ToolTip et les associer aux boutons.

$BoutonGaucheTooltip = New-Object System.Windows.Forms.ToolTip
$BoutonDroitTooltip = New-Object System.Windows.Forms.ToolTip
$BoutonGaucheTooltip.SetTooltip($BoutonGetUser, »Ce bouton permettait d’afficher le nom de l’utilisateur. »)
$BoutonDroitTooltip.SetTooltip($BoutonGetComp, »Ce bouton permettait d’afficher le nom de la machine. »)

Et le résultat :

Voici donc pour la deuxième partie de ma suite d’articles sur la création d’interfaces graphiques avec Powershell.

Liens

Télécharger le script de l’article
Développement d’une UI en Powershell : première partie – troisième partie
Documentation et référence des classes : MenuStripToolStripMenuItemGroupBox –  ToolTip

Powershell : création d’interfaces graphiques – première partie

Après quelques semaines de développement de ma Lightweight ActiveDirectory Toolbox, je vais commencer à faire une petite série d’articles afin d’expliquer comment développer une interface graphique avec Powershell, en utilisant Windows Forms. Cela est bien plus simple qu’il n’y paraît car nous allons écrire « beaucoup » de code ; une fois la logique acquise tout se fait très vite. J’ai la chance d’avoir fait quelques développements durant mes études et mon temps personnel il y a quelques années en VB.NET et C# avec WinForms, ce qui m’a facilité la tâche pour m’y remettre.

Dans cette première partie, nous allons aborder :

  • La conception d’une fenêtre ;
  • L’ajout de contrôles simples sur une fenêtre : label, button, textbox ;
  • L’ajout de EventsHandlers sur ces contrôles.
Si au début je développais avec Powershell ISE qui est intégré à Windows, j’ai fini par le remettre au placard suite à des freezes incessants et visiblement son incapacité à gérer des scripts qui ont quelques centaines de lignes. Sous Windows, je vous conseille l’excellent Notepad++ qui fera parfaitement le travail une fois le langage Powershell sélectionné en édition.
Quelques explications avant de se lancer, que vous pouvez passer si vous êtes déjà familiers avec WinForms ou le développement .NET en général :
  • Un contrôle est un élément visible de l’interface graphique. Il peut s’agir d’un bouton, d’une étiquette, d’un champ à remplir, etc.
  • Un EventHandler est une procédure qui sera déclenchée en fonction d’une action sur un contrôle : un clic, le passage de la souris, une modification de valeur, etc.
  • Une classe permet de définir un objet. Nous allons faire référence à des classes dans le code, à partir desquelles nous allons créer des objets. Par exemple, la classe Bouton va nous permettre de créer des boutons. Pour simplifier, la classe est le modèle et l’objet ce que nous modifions directement (taille, couleur, effet souhaité, etc.)
  • Il est important de bien nommer les variables : si cela ne pose pas de soucis d’avoir Bouton1, Bouton2 pour quelques lignes, un script ayant 40 ou 50 contrôles deviendra une plaie à écrire si l’on ne sait plus quel contrôle fait référence à Label4.
  • La documentation de Microsoft concernant WinForms est très riche et doit être le premier point de recherche en cas de doute.

Conception de la fenêtre de base

Tout d’abord, il faut savoir que Powershell tout seul ne sait pas créer d’interfaces graphiques avec WinForms. Il faut lui spécifier que nous allons appeler des classes propres à WinForms. En tout début de code, nous ajoutons ceci :

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()

Puis nous pouvons définir notre toute première fenêtre :

$Form = New-Object System.Windows.Forms.Form

Voilà, notre fenêtre existe. Mais elle n’a ni taille, ni titre, ni propriété aucune… Et elle ne s’affiche même pas ! Corrigeons cela :

$Form.ClientSize = ‘300,300’
$Form.Text = « Mon UI en PS »
$Form.ShowDialog()

A l’exécution, nous obtenons donc ceci :
Une belle fenêtre – vide – avec ses contrôles habituels pour la redimensionner, l’agrandir, la fermer, etc.

Ajout de contrôles supplémentaires

On va l’agrémenter d’un bouton et d’un label (élément de texte non modifiable par l’utilisateur). On reprend donc les mêmes idées que pour notre Form principale, c’est à dire création d’un objet de la classe Button et Label.

$Bouton = New-Object System.Windows.Forms.Button

Mais ceci ne suffira pas. Il nous faut au moins lui indiquer une taille, le texte apposé dessus et surtout… l’endroit où on veut le placer ! Pour définir l’emplacement d’un contrôle, il faut créer un objet avec les coordonnées. Windows dessinera alors le contrôle en partant des paramètres données. Ici, nous souhaitons donc dessiner le bouton à partir de 30 pixels du côté gauche de la fenêtre et 50 pixels du côté haut.

$Bouton.Location = New-Object System.Drawing.Point(30,50)

Et plus simplement, il ne reste plus qu’à lui donner ses attributs : une largeur, une hauteur et le texte écrit dessus.

$Bouton.Width = 80
$Bouton.Height = 40
$Bouton.Text = « Cliquez moi! »

Si vous exécutez maintenant le script, vous ne verrez pas votre bouton. En effet, comment Powershell peut-il savoir que le bouton que vous avez créé doit être affiché sur la Form ? Si vous ne lui dites pas, il peut pas. On va donc gentiment lui indiquer :

$Form.controls.Add($Bouton)

Ce qui va nous donner ceci :

Ajoutons une TextBox qui va servir à saisir du texte, puis une Label.

$TextBox = New-Object System.Windows.Forms.TextBox
$TextBox.Location = New-Object System.Drawing.Point(30,110)
$TextBox.Width = 70
$Label = New-Object System.Windows.Forms.Label
$Label.Location = New-Object System.Drawing.Point(30,150)
$Label.Text = « bonjour »

Etant donné que nous avons plusieurs contrôles désormais sur notre Form, plutôt que d’ajouter un contrôle par instruction, nous allons utiliser la variante AddRange et non Add de $Form.controls.

Ainsi, nous remplaçons $Form.controls.Add($Bouton) par :

$Form.controls.AddRange(@($Bouton,$Label,$Textbox))

Et à l’exécution, tout apparaît :

Attention à bien renseigner les coordonnées des contrôles car si ils se chevauchent, ils peuvent ne plus être visibles et vous faire chercher à comprendre pourquoi ce satané Label ne s’affiche pas alors qu’il est « coincé » sous le bouton…

Tout cela est bien beau, mais n’offre aucune interactivité. Nous allons faire en sorte qu’un clic sur le bouton affiche sur le Label le texte saisi dans notre TextBox.

Ajout d’EventHandlers

C’est ici que le véritable développement Powershell se fait. C’est sur ces EventHandlers que l’on place le code qui est traité par le script. Chaque objet possède ses propres EventHandlers mais pas mal sont communs entre eux, dont le Add_Click que je vais présenter. Celui-ci permet de réagir lorsque l’utilisateur clique sur le contrôle. Nous allons donc écrire un EventHandler sur le bouton :

$Bouton.Add_Click({ })

Et glisser le code entre les accolades. Dans cet exemple, je veux prendre le contenu de la TextBox et l’affiche sur le label. J’obtiens donc simplement ceci :

$Bouton.Add_Click({
Label.Text = TextBox.Text
})

Il est important de toujours garder en dernière ligne l’ouverture de la Form car le script se met en pause de déroulement lorsque la fenêtre est activée (en attente d’un EventHandler, justement). Afin que tout soit bien interprété, on placera nos EventHandler avant.
Sur un clic du bouton, le Label prend le texte indiqué dans la TextBox. Réalisons un deuxième bouton qui viendra écrire la date du jour dans le label lors du clic.

$Bouton2 = New-Object System.Windows.Forms.Button
$Bouton2.Location = New-Object System.Drawing.Point(150,50)
$Bouton2.Width = 80
$Bouton2.Height = 40
$Bouton2.Text = « date du jour, bonjour! »

On ne manquera pas d’ajouter $Bouton2 dans le $Form.Controls.AddRange afin qu’il apparaisse sur la Form. Puis, on écrit son EventHandler :

$Bouton2.Add_Click({
$Label.Text = Get-Date
})

Ce qui nous donne ceci :
Voici donc pour cette première partie qui illustre les bases de la création d’une interface graphique en Powershell grâce à Windows Forms.

Liens

Télécharger le script de l’article
Développement d’une UI en Powershell : deuxième partie – troisième partie
Documentation et référence des classes : FormButtonLabelTextBox