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 partie - deuxième partie
Documentation et références des classes : DataGridView - DataGridViewRow - DataGridViewColumn - DataGridViewCell

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 : MenuStrip - ToolStripMenuItem - GroupBox -  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 : Form - Button - Label - TextBox