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

Powershell : fermeture de session inactives à distance

Suite à un besoin récurrent, j’ai développé un script Powershell permettant de mettre fin à tous les sessions déclarées comme inactives sur un serveur Windows distant. Il est constitué d’une fonction qui est chargée de terminer les sessions et de quelques instructions appelant cette fonction ; une fois un serveur traité, le script demande si l’utilisateur souhaite traiter un autre serveur.

A noter qu’il faut naturellement des droits d’administration sur les serveurs sur lesquels une session doit être terminée.

$kick = "Y"
function Kick {
param ($srv)
$quout = quser /server:$srv
$status = "Disc"
$indexs = 2
$indexu = 1
$sid = (($quout | Where-Object { $_ -match $status }) -split ' +')[2]
while ($sid -ne $null)
{
$username = (($quout | Where-Object { $_ -match $status }) -split ' +')[$indexu]
$sid = (($quout | Where-Object { $_ -match $status }) -split ' +')[$indexs]
if ($sid -eq $null) { break }
Write-Host "User :"$username
Invoke-RDUserLogoff -HostServer $srv -Unifiedsid $sid -Force
$indexu = $indexu+8
$indexs = $indexs+8
Write-Host "Session has been terminated."
Start-Sleep -Seconds 3
}
}
Write-Host "Force close disconnected session script for Windows 2008 and 2012"
while ($kick -eq "Y")
{
$srv = Read-Host "Server Name"
kick($srv)
$kick = Read-Host "Do you want to process another server ? Y/N"
}

Powershell : obtention de la station qui déclenche un verrouillage de compte AD

Ayant eu un cas récalcitrant de compte qui se verrouillait a priori tout seul, j’ai créé un petit script PowerShell à lancer en tant qu’administrateur sur un contrôleur de domaine. Ce script parcourt le journal d’événements de sécurité afin d’y trouver une entrée avec un identifiant 4740 correspondant à l’identifiant passé en paramètre.

Le script est disponible dans une version commentée sur mon miroir de téléchargement.

param
([string]$User)

$EventLog = Get-EventLog -LogName « Security » -InstanceID « 4740 » -Message « *$User* » -Newest 1
if ($EventLog -eq $null) {
Write-Host « User doesn’t exist or there is no log entry related to it. »
Break
}
Write-Host $EventLog[0].TimeGenerated
Write-Host $EventLog[0].Message

On appellera donc ensuite le script comme ceci :

PS > win_event4740.ps1 usertest

Par exemple, on peut utiliser LockoutStatus pour déterminer le DC qui procède au verrouillage du compte :

Sur cette capture, le compte n’est pas verrouillé, mais si il l’est, la colonne encadrée donnera le nom du DC ayant procédé au blocage du compte.

Ensuite, en se connectant au domain controller en question, l’exécution du script en passant en paramètre le login du compte verrouillé évitera d’ouvrir le journal d’évenements et d’avoir à filtrer parmi plusieurs évenements de verrouillages de compte.