Files
DeployCenter/DeployCenter.ps1
T
2026-10-15 21:15:33 +02:00

1818 lines
79 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DeployCenter - Interface WinPE
# Compatible Windows PowerShell 5.1
param(
# Affiche DeployCenter dans une fenetre redimensionnable pour le debug.
[switch]$Windowed,
[switch]$Vnc
)
$script:Windowed = [bool]$Windowed
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
Import-Module (Join-Path $PSScriptRoot "Modules\ThemeManager.psm1") -Force
Import-Module (Join-Path $PSScriptRoot "Modules\UI.psm1") -Force
# GroupBox dessine par DeployCenter pour que le fond et le cadre suivent les themes.
if (-not ([System.Management.Automation.PSTypeName]'DeployCenter.ThemedGroupBox').Type) {
Add-Type -TypeDefinition @'
using System;
using System.Drawing;
using System.Windows.Forms;
namespace DeployCenter {
public class ThemedGroupBox : GroupBox {
public Color PanelColor { get; set; }
public Color BorderColor { get; set; }
public Color TextColor { get; set; }
public ThemedGroupBox() {
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
PanelColor = SystemColors.Control;
BorderColor = SystemColors.ControlDark;
TextColor = SystemColors.ControlText;
}
protected override void OnPaint(PaintEventArgs e) {
Graphics g = e.Graphics;
g.Clear(PanelColor);
Size textSize = TextRenderer.MeasureText(Text, Font);
int textLeft = 10;
int lineY = Math.Max(8, (int)Math.Ceiling(textSize.Height / 2.0));
using (Pen pen = new Pen(BorderColor)) {
g.DrawLine(pen, 0, lineY, textLeft - 2, lineY);
int afterText = textLeft + textSize.Width + 4;
if (afterText < Width - 2) g.DrawLine(pen, afterText, lineY, Width - 2, lineY);
g.DrawLine(pen, 0, lineY, 0, Height - 2);
g.DrawLine(pen, 0, Height - 2, Width - 2, Height - 2);
g.DrawLine(pen, Width - 2, lineY, Width - 2, Height - 2);
}
TextRenderer.DrawText(g, Text, Font, new Point(textLeft, 0), TextColor,
PanelColor, TextFormatFlags.NoPadding | TextFormatFlags.SingleLine);
}
}
}
'@ -ReferencedAssemblies System.Windows.Forms,System.Drawing
}
# ============================================================
# CHEMINS
# ============================================================
$script:DeployRoot = $PSScriptRoot
# Racine du lecteur qui heberge DeployCenter (ex. Y:\), sans lecteur code en dur.
$script:DeployVolumeRoot = $null
try {
$deployQualifier = Split-Path -Path $script:DeployRoot -Qualifier -ErrorAction Stop
if (-not [string]::IsNullOrWhiteSpace($deployQualifier)) {
$script:DeployVolumeRoot = $deployQualifier.TrimEnd('\') + '\'
}
} catch {
$script:DeployVolumeRoot = $null
}
$script:HardwareScript = Join-Path $script:DeployRoot "HardwareInfo.ps1"
$script:AssetsRoot = Join-Path $script:DeployRoot "Assets"
$script:LogoPath = Join-Path $script:AssetsRoot "logo.png"
$script:LogoDarkPath = Join-Path $script:AssetsRoot "logo-dark.png"
$script:LogRoot = Join-Path $script:DeployRoot "Logs"
# Détection réseau centralisée dans Modules\\Network.psm1.
$networkModulePath = Join-Path $PSScriptRoot "Modules\\Network.psm1"
if (-not (Test-Path -LiteralPath $networkModulePath -PathType Leaf)) {
throw "Module réseau introuvable : $networkModulePath"
}
. ([scriptblock]::Create([System.IO.File]::ReadAllText($networkModulePath)))
$script:DeployIPv4 = Get-DeployNetworkIPv4
# La gestion des journaux est centralisee dans Modules\\Logging.psm1.
$loggingModulePath = Join-Path $PSScriptRoot "Modules\\Logging.psm1"
if (-not (Test-Path -LiteralPath $loggingModulePath -PathType Leaf)) {
throw "Module de journalisation introuvable : $loggingModulePath"
}
. ([scriptblock]::Create([System.IO.File]::ReadAllText($loggingModulePath)))
Initialize-DeployCenterLogging -LogRoot $script:LogRoot -IPv4 $script:DeployIPv4
$script:HardwareJson = Join-Path $env:TEMP "HardwareInfo.json"
# Inventaire de session et UltraVNC portable : tout reste dans le dossier temporaire.
$script:InventoryDir = Join-Path $env:TEMP 'DeployCenter'
$script:InventoryFile = Join-Path $script:InventoryDir 'DeployCenter_Inventory.json'
$script:VncPassword = $null
$script:VncDirectory = Join-Path $script:InventoryDir ("uvnc_{0}" -f $PID)
$script:VncProcess = $null
$script:VncStarted = $false
$script:HardwareInventory = $null
# ============================================================
# THEMES EXTERNES (Themes\*.psd1)
# Les thèmes sont des fichiers de données ; aucun code PowerShell n'est exécuté.
# ============================================================
$script:ThemesRoot = Join-Path $script:DeployRoot "Themes"
if (-not (Test-Path -LiteralPath $script:ThemesRoot -PathType Container)) {
New-Item -Path $script:ThemesRoot -ItemType Directory -Force | Out-Null
}
# ============================================================
# PREFERENCES (DeployCenter.ini en lecture seule : aucune ecriture automatique)
# ============================================================
$script:ConfigPath = Join-Path $script:DeployRoot "DeployCenter.ini"
$script:Config = @{
Theme = "Light"
SourcePaths = "%DEPLOY_VOLUME%"
VncPassword = ""
}
function Read-DeployCenterConfig {
if (-not (Test-Path -LiteralPath $script:ConfigPath -PathType Leaf)) { return }
$section = ""
foreach ($line in (Get-Content -LiteralPath $script:ConfigPath -ErrorAction SilentlyContinue)) {
$trimmed = $line.Trim()
if (-not $trimmed -or $trimmed.StartsWith(";") -or $trimmed.StartsWith("#")) { continue }
if ($trimmed -match '^\[(.+)\]$') { $section = $matches[1].Trim(); continue }
if ($trimmed -match '^([^=]+)=(.*)$') {
$key = $matches[1].Trim()
$value = $matches[2].Trim()
if ($section -ieq "General" -and $key -ieq "Theme") {
if ($value -match '^[A-Za-z0-9_-]+$' -and (Test-Path -LiteralPath (Join-Path $script:ThemesRoot ($value + ".psd1")) -PathType Leaf)) {
$script:Config.Theme = $value
}
}
elseif ($section -ieq "Sources" -and $key -ieq "Paths") {
$script:Config.SourcePaths = $value
}
elseif ($section -ieq "UltraVNC" -and $key -ieq "Password") {
# Mot de passe facultatif pour les tests ; s'il est renseigné, il remplace la génération aléatoire.
$script:Config.VncPassword = $value
}
}
}
}
# Themes par defaut : crees uniquement si absents, afin de faciliter les personnalisations.
$defaultThemeFiles = @{
Dark = @"
@{
Background = "#171A21"
Panel = "#222731"
PanelAlt = "#1C2029"
Text = "#F1F5F9"
Secondary = "#9CA9BC"
Accent = "#3B82F6"
Success = "#4ADE80"
Border = "#414A59"
Danger = "#EF4444"
Button = @{
Background = "#303744"; Foreground = "#F1F5F9"; Border = "#414A59"
HoverBackground = "#3B4555"; PressedBackground = "#485365"
DisabledBackground = "#292E38"; DisabledForeground = "#7F8999"
}
PrimaryButton = @{
Background = "#3B82F6"; Foreground = "#FFFFFF"; Border = "#3B82F6"
HoverBackground = "#2563EB"; PressedBackground = "#1D4ED8"
DisabledBackground = "#344A6A"; DisabledForeground = "#9AAAC1"
}
DangerButton = @{
Background = "#EF4444"; Foreground = "#FFFFFF"; Border = "#EF4444"
HoverBackground = "#DC2626"; PressedBackground = "#B91C1C"
DisabledBackground = "#59383B"; DisabledForeground = "#BBA0A2"
}
}
"@
Light = @"
@{
Background = "#F5F7FA"
Panel = "#FFFFFF"
PanelAlt = "#F8FAFC"
Text = "#1E293B"
Secondary = "#475569"
Accent = "#2563EB"
Success = "#16A34A"
Border = "#CBD5E1"
Danger = "#DC2626"
Button = @{
Background = "#E8EDF3"; Foreground = "#1E293B"; Border = "#CBD5E1"
HoverBackground = "#DCE4EE"; PressedBackground = "#CBD5E1"
DisabledBackground = "#F1F3F6"; DisabledForeground = "#94A3B8"
}
PrimaryButton = @{
Background = "#2563EB"; Foreground = "#FFFFFF"; Border = "#2563EB"
HoverBackground = "#1D4ED8"; PressedBackground = "#1E40AF"
DisabledBackground = "#C7D8F8"; DisabledForeground = "#F8FAFC"
}
DangerButton = @{
Background = "#DC2626"; Foreground = "#FFFFFF"; Border = "#DC2626"
HoverBackground = "#B91C1C"; PressedBackground = "#991B1B"
DisabledBackground = "#F2C5C5"; DisabledForeground = "#FFF7F7"
}
}
"@
}
foreach ($themeFileName in $defaultThemeFiles.Keys) {
$themeFilePath = Join-Path $script:ThemesRoot ($themeFileName + '.psd1')
if (-not (Test-Path -LiteralPath $themeFilePath -PathType Leaf)) {
Set-Content -LiteralPath $themeFilePath -Value $defaultThemeFiles[$themeFileName] -Encoding UTF8
}
}
Read-DeployCenterConfig
$availableThemes = @(Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)
if ($availableThemes.Count -eq 0) { throw "Aucun thème valide trouvé dans $script:ThemesRoot" }
if ($availableThemes -notcontains $script:Config.Theme) { $script:Config.Theme = "Light" }
$script:CurrentTheme = $script:Config.Theme
try {
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName $script:CurrentTheme
} catch {
$script:CurrentTheme = "Light"
$script:Config.Theme = "Light"
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName "Light"
}
# ============================================================
# POLICES
# ============================================================
$script:FontRegular = [System.Drawing.Font]::new("Segoe UI", [single]10, [System.Drawing.FontStyle]::Regular)
$script:FontSmall = [System.Drawing.Font]::new("Segoe UI", [single]9, [System.Drawing.FontStyle]::Regular)
$script:FontTitle = [System.Drawing.Font]::new("Segoe UI", [single]23, [System.Drawing.FontStyle]::Bold)
$script:FontSubtitle = [System.Drawing.Font]::new("Segoe UI", [single]10, [System.Drawing.FontStyle]::Regular)
$script:FontGroup = [System.Drawing.Font]::new("Segoe UI", [single]11, [System.Drawing.FontStyle]::Bold)
# ============================================================
# RESEAU, ULTRAVNC ET INVENTAIRE DE SESSION
# ============================================================
function Write-DeployInventory {
param($HardwareInfo = $null)
try {
if (-not (Test-Path -LiteralPath $script:InventoryDir -PathType Container)) {
New-Item -Path $script:InventoryDir -ItemType Directory -Force | Out-Null
}
if ($null -ne $HardwareInfo) { $script:HardwareInventory = $HardwareInfo }
$inventory = [ordered]@{
GeneratedAt = (Get-Date).ToString('o')
DeploymentIPv4 = $script:DeployIPv4
VncPassword = $script:VncPassword
Hardware = $script:HardwareInventory
}
$json = $inventory | ConvertTo-Json -Depth 8
Set-Content -LiteralPath $script:InventoryFile -Value $json -Encoding UTF8
} catch {
Write-Log "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING'
}
}
function Set-DeployCenterFormSizing {
param(
[Parameter(Mandatory=$true)][System.Windows.Forms.Form]$Form,
[switch]$Resizable
)
if ($Resizable) {
$Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
$Form.MaximizeBox = $true
} else {
$Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$Form.MaximizeBox = $false
}
}
function Add-SessionFooter {
param(
[Parameter(Mandatory=$true)][System.Windows.Forms.Form]$TargetForm,
[bool]$ShowFooter = $true,
[bool]$ShowPassword = $true
)
if (-not $ShowFooter) { return }
$footer = New-Object System.Windows.Forms.Label
$footer.Name = 'DeployCenterSessionFooter'
$footer.Text = if ($script:DeployIPv4) {
"IP de déploiement : $($script:DeployIPv4)"
} else {
'IP de déploiement indisponible'
}
if ($ShowPassword -and -not [string]::IsNullOrWhiteSpace([string]$script:VncPassword)) {
$footer.Text += " | Mot de passe VNC : $($script:VncPassword)"
}
$footer.Height = 26
$footer.Dock = [System.Windows.Forms.DockStyle]::Bottom
$footer.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$footer.Padding = New-Object System.Windows.Forms.Padding(12, 0, 0, 0)
$footer.BackColor = $script:Theme.PanelAlt
$footer.ForeColor = $script:Theme.Text
$footer.Font = $script:FontSmall
$TargetForm.Controls.Add($footer)
$footer.BringToFront()
}
# L'adresse IPv4 a été détectée par Modules\\Network.psm1 avant l'initialisation du journal.
# VNC est désactivé par défaut ; il démarre uniquement avec le paramètre -vnc.
$script:EnableVnc = [bool]$Vnc
if ($script:EnableVnc) {
try {
# Le code VNC est exécuté dans le scope principal uniquement avec -vnc.
$vncModulePath = Join-Path $PSScriptRoot "Modules\VNC.psm1"
if (-not (Test-Path -LiteralPath $vncModulePath -PathType Leaf)) {
throw "Module VNC introuvable : $vncModulePath"
}
$vncCode = [System.IO.File]::ReadAllText($vncModulePath)
. ([scriptblock]::Create($vncCode))
Start-DeployCenterVnc
} catch {
Write-Log "Impossible de démarrer VNC : $($_.Exception.Message)" 'ERROR'
}
} else {
Write-Log "UltraVNC non démarré : paramètre -vnc absent."
}
# ============================================================
# CREATION DES CONTROLES
# ============================================================
function New-Label {
param(
[string]$Text,
[int]$X,
[int]$Y,
[int]$Width = 180,
[int]$Height = 24,
$Color = $null,
$Font = $null
)
$label = New-Object System.Windows.Forms.Label
$label.Text = $Text
$label.Location = New-Object System.Drawing.Point($X, $Y)
$label.Size = New-Object System.Drawing.Size($Width, $Height)
$label.AutoEllipsis = $true
$label.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$label.BackColor = [System.Drawing.Color]::Transparent
if ($null -eq $Color) {
$label.ForeColor = $script:Theme.Text
}
else {
$label.ForeColor = $Color
}
if ($null -eq $Font) {
$label.Font = $script:FontRegular
}
else {
$label.Font = $Font
}
return $label
}
function New-GroupBox {
param(
[string]$Text,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height
)
$group = New-Object DeployCenter.ThemedGroupBox
$group.Text = $Text
$group.Location = New-Object System.Drawing.Point($X, $Y)
$group.Size = New-Object System.Drawing.Size($Width, $Height)
$group.ForeColor = $script:Theme.Text
$group.BackColor = $script:Theme.Panel
$group.PanelColor = $script:Theme.Panel
$group.BorderColor = $script:Theme.Border
$group.TextColor = $script:Theme.Text
$group.Font = $script:FontGroup
return $group
}
function New-Button {
param(
[string]$Text,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height,
[ValidateSet("Button", "PrimaryButton", "DangerButton")][string]$Role = "Button"
)
$button = New-Object System.Windows.Forms.Button
$button.Text = $Text
$button.Location = New-Object System.Drawing.Point($X, $Y)
$button.Size = New-Object System.Drawing.Size($Width, $Height)
$button.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$button.FlatAppearance.BorderSize = 1
$button.Tag = $Role
Set-DeployCenterButtonStyle -Button $button -Theme $script:Theme -Role $Role
$button.Font = $script:FontRegular
$button.Cursor = [System.Windows.Forms.Cursors]::Hand
$button.UseVisualStyleBackColor = $false
$button.AutoSize = $false
$button.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
return $button
}
# ============================================================
# FENETRE PRINCIPALE
# ============================================================
$script:form = New-Object System.Windows.Forms.Form
$script:form.Text = "DeployCenter"
$script:form.BackColor = $script:Theme.Background
if ($script:Windowed) {
# Mode debug : fenetre classique, deplacable et reductible.
$script:form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
$script:form.WindowState = [System.Windows.Forms.FormWindowState]::Normal
$script:form.TopMost = $false
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
$debugWidth = [Math]::Min(1200, [Math]::Max(800, $screen.Width - 80))
$debugHeight = [Math]::Min(850, [Math]::Max(600, $screen.Height - 80))
$script:form.Size = New-Object System.Drawing.Size($debugWidth, $debugHeight)
} else {
# Mode normal : plein ecran, sans bordure et au premier plan.
$script:form.StartPosition = "Manual"
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
$script:form.WindowState = [System.Windows.Forms.FormWindowState]::Maximized
$script:form.TopMost = $true
$script:form.Bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
}
$script:form.ForeColor = $script:Theme.Text
$script:form.Font = $script:FontRegular
$script:form.KeyPreview = $true
$script:form.MinimumSize = New-Object System.Drawing.Size(800, 600)
$screenWidth = $script:form.ClientSize.Width
$screenHeight = $script:form.ClientSize.Height
$margin = 30
$usableWidth = $screenWidth - (2 * $margin)
# ============================================================
# EN-TETE ET LOGO FACULTATIF
# ============================================================
$script:pictureLogo = $null
$titleX = 30
# Deux fichiers seulement : logo.png (clair et solution de repli) et logo-dark.png.
# En mode sombre, logo-dark.png est prioritaire ; s'il manque, logo.png est utilise.
# En mode clair, logo.png est utilise.
function Get-LogoPathForTheme {
param([string]$ThemeName)
if ($ThemeName -eq "Dark" -and (Test-Path $script:LogoDarkPath)) {
return $script:LogoDarkPath
}
if (Test-Path $script:LogoPath) {
return $script:LogoPath
}
# Si logo.png manque, garder logo-dark.png comme dernier recours.
if (Test-Path $script:LogoDarkPath) {
return $script:LogoDarkPath
}
return $null
}
function Update-DeployCenterLogo {
param([string]$ThemeName)
if ($null -eq $script:pictureLogo) { return }
$logoPath = Get-LogoPathForTheme -ThemeName $ThemeName
if ([string]::IsNullOrWhiteSpace($logoPath)) { return }
try {
$newImage = [System.Drawing.Image]::FromFile($logoPath)
$oldImage = $script:pictureLogo.Image
$script:pictureLogo.Image = $newImage
if ($null -ne $oldImage) { $oldImage.Dispose() }
$script:pictureLogo.Visible = $true
}
catch {
# En cas d'image absente ou invalide, conserver le logo deja affiche.
}
}
$initialLogoPath = Get-LogoPathForTheme -ThemeName $script:CurrentTheme
if ($null -ne $initialLogoPath) {
try {
$script:pictureLogo = New-Object System.Windows.Forms.PictureBox
$script:pictureLogo.Location = New-Object System.Drawing.Point(30, 20)
$script:pictureLogo.Size = New-Object System.Drawing.Size(64, 64)
$script:pictureLogo.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
$script:pictureLogo.BackColor = [System.Drawing.Color]::Transparent
$script:pictureLogo.Image = [System.Drawing.Image]::FromFile($initialLogoPath)
$script:form.Controls.Add($script:pictureLogo)
$titleX = 110
}
catch {
$script:pictureLogo = $null
$titleX = 30
}
}
$script:lblTitle = New-Label -Text "DeployCenter" `
-X $titleX -Y 17 -Width 600 -Height 42 `
-Color $script:Theme.Text -Font $script:FontTitle
$script:lblSubtitle = New-Label `
-Text "Environnement de déploiement Windows" `
-X ($titleX + 4) -Y 61 -Width 650 -Height 24 `
-Color $script:Theme.Secondary -Font $script:FontSubtitle
$script:form.Controls.Add($script:lblTitle)
$script:form.Controls.Add($script:lblSubtitle)
# Menu de selection du theme ; les noms correspondent aux fichiers Themes\<Nom>.psd1
$script:cmbTheme = New-Object System.Windows.Forms.ComboBox
$script:cmbTheme.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
# OwnerDraw permet de colorer aussi la liste deroulante, que WinForms laisse sinon au theme systeme.
$script:cmbTheme.DrawMode = [System.Windows.Forms.DrawMode]::OwnerDrawFixed
$script:cmbTheme.ItemHeight = 24
$script:cmbTheme.Font = $script:FontRegular
$script:cmbTheme.Width = 190
$script:cmbTheme.Height = 32
$script:cmbTheme.Location = New-Object System.Drawing.Point(0, 29)
$script:cmbTheme.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
foreach ($themeName in (Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)) { [void]$script:cmbTheme.Items.Add($themeName) }
$script:cmbTheme.SelectedItem = $script:CurrentTheme
$script:cmbTheme.BackColor = $script:Theme.Panel
$script:cmbTheme.ForeColor = $script:Theme.Text
$script:cmbTheme.Add_DrawItem({
param($sender, $e)
if ($e.Index -lt 0) {
$itemText = [string]$sender.Text
} else {
$itemText = [string]$sender.Items[$e.Index]
}
$isSelected = (($e.State -band [System.Windows.Forms.DrawItemState]::Selected) -ne 0)
if ($isSelected) {
$backgroundColor = $script:Theme.Accent
$foregroundColor = [System.Drawing.Color]::White
} else {
$backgroundColor = $script:Theme.Panel
$foregroundColor = $script:Theme.Text
}
$backgroundBrush = New-Object System.Drawing.SolidBrush($backgroundColor)
try { $e.Graphics.FillRectangle($backgroundBrush, $e.Bounds) }
finally { $backgroundBrush.Dispose() }
if (-not [string]::IsNullOrEmpty($itemText)) {
$textBounds = $e.Bounds
$textBounds.X += 4
[System.Windows.Forms.TextRenderer]::DrawText(
$e.Graphics, $itemText, $sender.Font, $textBounds, $foregroundColor,
([System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis)
)
}
$e.DrawFocusRectangle()
})
$script:form.Controls.Add($script:cmbTheme)
$script:cmbTheme.BringToFront()
# ============================================================
# INFORMATIONS DU PC
# ============================================================
$script:groupPC = New-GroupBox -Text "Informations du PC" `
-X $margin -Y 100 -Width $usableWidth -Height 105
$script:lblManufacturer = New-Label -Text "Fabricant : -" `
-X 18 -Y 25 -Width 330 -Height 24
$script:lblModel = New-Label -Text "Modèle : -" `
-X 360 -Y 25 -Width 400 -Height 24
$script:lblSerial = New-Label -Text "Numéro de série : -" `
-X 18 -Y 52 -Width 330 -Height 24
$script:lblFirmware = New-Label -Text "Firmware : -" `
-X 360 -Y 52 -Width 220 -Height 24
$script:lblArchitecture = New-Label -Text "Architecture : -" `
-X 600 -Y 52 -Width 220 -Height 24
$script:lblWinPE = New-Label -Text "Environnement : -" `
-X 18 -Y 78 -Width 350 -Height 22 `
-Color $script:Theme.Secondary -Font $script:FontSmall
$script:groupPC.Controls.AddRange(@(
$script:lblManufacturer,
$script:lblModel,
$script:lblSerial,
$script:lblFirmware,
$script:lblArchitecture,
$script:lblWinPE
))
$script:form.Controls.Add($script:groupPC)
# ============================================================
# DISQUE PHYSIQUE - SELECTEUR COMPACT
# ============================================================
$script:groupDisks = New-GroupBox -Text "Disque physique" `
-X $margin -Y 220 -Width $usableWidth -Height 78
$script:cmbDisk = New-Object System.Windows.Forms.ComboBox
$script:cmbDisk.Name = "cmbDisk"
$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30)
$script:cmbDisk.Size = New-Object System.Drawing.Size(($usableWidth - 24), 28)
$script:cmbDisk.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
$script:cmbDisk.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
$script:cmbDisk.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$script:cmbDisk.BackColor = $script:Theme.PanelAlt
$script:cmbDisk.ForeColor = $script:Theme.Text
$script:cmbDisk.Font = $script:FontSmall
$script:cmbDisk.IntegralHeight = $false
$script:cmbDisk.MaxDropDownItems = 12
$script:groupDisks.Controls.Add($script:cmbDisk)
$script:form.Controls.Add($script:groupDisks)
function Set-DiskSelectorWidth {
if ($null -eq $script:cmbDisk -or $null -eq $script:groupDisks) { return }
$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30)
$script:cmbDisk.Size = New-Object System.Drawing.Size([Math]::Max(250, $script:groupDisks.ClientSize.Width - 24), 28)
}
# ============================================================
# JOURNAL
# ============================================================
$logY = $script:groupDisks.Bottom + 15
$buttonHeight = 38
# En plein ecran, rapprocher les boutons du bas sans empiéter sur le pied de page.
# En mode fenetre, conserver la position actuelle.
$buttonY = if ($script:Windowed) {
$screenHeight - 54 - $buttonHeight
} else {
$screenHeight - 42 - $buttonHeight
}
$logHeight = [math]::Max(100, $buttonY - $logY - 15)
$script:groupLog = New-GroupBox -Text "Journal" `
-X $margin -Y $logY -Width $usableWidth -Height $logHeight
$script:txtLog = New-Object System.Windows.Forms.TextBox
$script:txtLog.Location = New-Object System.Drawing.Point(12, 28)
$script:txtLog.Size = New-Object System.Drawing.Size(
($usableWidth - 28), ($logHeight - 44)
)
$script:txtLog.Multiline = $true
$script:txtLog.ReadOnly = $true
$script:txtLog.WordWrap = $false
$script:txtLog.ScrollBars = [System.Windows.Forms.ScrollBars]::Both
$script:txtLog.BackColor = $script:Theme.PanelAlt
$script:txtLog.ForeColor = $script:Theme.Text
$script:txtLog.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
$script:txtLog.Font = [System.Drawing.Font]::new("Consolas", [single]9, [System.Drawing.FontStyle]::Regular)
$script:groupLog.Controls.Add($script:txtLog)
$script:form.Controls.Add($script:groupLog)
# ============================================================
# BOUTONS ACTUALISER ET FERMER
# ============================================================
$buttonWidth = 140
$buttonGap = 12
$closeX = $screenWidth - $margin - $buttonWidth
$refreshX = $closeX - $buttonGap - $buttonWidth
$sourcesX = $refreshX - $buttonGap - 170
$script:btnSources = New-Button -Text "Sources Windows" `
-X $sourcesX -Y $buttonY -Width 170 -Height $buttonHeight `
-Role "Button"
$script:btnSources.ForeColor = $script:Theme.Text
$script:btnRefresh = New-Button -Text "Actualiser" `
-X $refreshX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Role "PrimaryButton"
$script:btnClose = New-Button -Text "Fermer" `
-X $closeX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Role "DangerButton"
$script:form.Controls.Add($script:btnSources)
$script:form.Controls.Add($script:btnRefresh)
$script:form.Controls.Add($script:btnClose)
Add-SessionFooter -TargetForm $script:form -ShowPassword $true
# ============================================================
# APPLICATION DU THEME A TOUS LES CONTROLES
# ============================================================
function Set-ControlTheme {
param([System.Windows.Forms.Control]$Control)
# Le formulaire principal doit lui aussi changer de fond.
if ($Control -is [System.Windows.Forms.Form]) {
$Control.BackColor = $script:Theme.Background
$Control.ForeColor = $script:Theme.Text
}
elseif ($Control -is [DeployCenter.ThemedGroupBox]) {
$Control.BackColor = $script:Theme.Panel
$Control.PanelColor = $script:Theme.Panel
$Control.BorderColor = $script:Theme.Border
$Control.TextColor = $script:Theme.Text
$Control.ForeColor = $script:Theme.Text
$Control.Invalidate()
}
elseif ($Control -is [System.Windows.Forms.Label]) {
$Control.ForeColor = $script:Theme.Text
# Le pied de page VNC suit lui aussi la palette du thème actif.
if ($Control.Name -eq 'DeployCenterSessionFooter') {
$Control.BackColor = $script:Theme.PanelAlt
$Control.ForeColor = $script:Theme.Secondary
$Control.Invalidate()
}
}
elseif ($Control -is [System.Windows.Forms.ListView]) {
$Control.BackColor = $script:Theme.PanelAlt
$Control.ForeColor = $script:Theme.Text
}
elseif ($Control -is [System.Windows.Forms.TextBox]) {
$Control.BackColor = $script:Theme.PanelAlt
$Control.ForeColor = $script:Theme.Text
}
foreach ($child in $Control.Controls) {
Set-ControlTheme -Control $child
}
}
function Set-DeployCenterTheme {
param([Parameter(Mandatory=$true)][string]$ThemeName)
$availableThemes = @(Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)
if ($availableThemes -notcontains $ThemeName) { return }
try {
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName $ThemeName
} catch {
Write-Log ("Impossible de charger le theme '$ThemeName' : " + $_.Exception.Message) "ERROR"
return
}
$script:CurrentTheme = $ThemeName
$script:cmbTheme.BackColor = $script:Theme.Panel
$script:cmbTheme.ForeColor = $script:Theme.Text
$script:cmbTheme.Invalidate()
$script:cmbTheme.Refresh()
Set-ControlTheme -Control $script:form
# Met a jour le logo si une variante propre au theme existe.
Update-DeployCenterLogo -ThemeName $script:CurrentTheme
$script:lblTitle.ForeColor = $script:Theme.Text
$script:lblSubtitle.ForeColor = $script:Theme.Secondary
if ($script:lblWinPE.Text -like "*WinPE*") {
$script:lblWinPE.ForeColor = $script:Theme.Success
}
else {
$script:lblWinPE.ForeColor = $script:Theme.Secondary
}
Set-DeployCenterButtonStyle -Button $script:btnSources -Theme $script:Theme -Role 'Button'
Set-DeployCenterButtonStyle -Button $script:btnRefresh -Theme $script:Theme -Role 'PrimaryButton'
Set-DeployCenterButtonStyle -Button $script:btnClose -Theme $script:Theme -Role 'DangerButton'
# Actualise explicitement le pied de page principal lors du changement de thème.
foreach ($footer in $script:form.Controls.Find('DeployCenterSessionFooter', $true)) {
$footer.BackColor = $script:Theme.PanelAlt
$footer.ForeColor = $script:Theme.Secondary
$footer.Invalidate()
}
$script:cmbTheme.BackColor = $script:Theme.Panel
$script:cmbTheme.ForeColor = $script:Theme.Text
$script:cmbTheme.Invalidate()
$script:groupPC.Invalidate()
$script:groupDisks.Invalidate()
$script:groupLog.Invalidate()
$script:cmbDisk.BackColor = $script:Theme.PanelAlt
$script:cmbDisk.ForeColor = $script:Theme.Text
$script:cmbDisk.Invalidate()
# Le bouton reste au-dessus des autres controles de l'en-tete.
$script:cmbTheme.BringToFront()
# Forcer le rafraichissement apres le changement de palette.
$script:form.SuspendLayout()
try {
foreach ($control in $script:form.Controls) {
$control.Invalidate($true)
}
$script:form.Invalidate($true)
$script:form.Refresh()
}
finally {
$script:form.ResumeLayout($true)
}
}
# ============================================================
# DETECTION DES SOURCES WINDOWS SUR PARTAGE SMB
# ============================================================
function Find-WindowsSources {
param([Parameter(Mandatory=$true)][string]$RootPath)
$results = New-Object System.Collections.Generic.List[object]
$roots = @($RootPath -split ';' | ForEach-Object {
$candidate = $_.Trim().Trim('"')
if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') }
$candidate
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
if ($roots.Count -eq 0) {
throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.'
}
foreach ($root in $roots) {
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
Write-Log "Source inaccessible, ignoree : $root" 'WARNING'
continue
}
Write-Log "Recherche des sources Windows dans : $root"
# Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees.
# Afficher les ISO et les fichiers WIM/ESD autonomes.
# Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un
# support Windows extrait dont le même dossier Sources contient install.wim/esd.
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
$files = New-Object System.Collections.Generic.List[object]
foreach ($file in $allFiles) {
if ($file.Extension -ieq '.iso') {
[void]$files.Add($file)
continue
}
if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') {
continue
}
$isBootWimInExtractedMedia = $false
if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') {
$installWim = Join-Path $file.Directory.FullName 'install.wim'
$installEsd = Join-Path $file.Directory.FullName 'install.esd'
if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or
(Test-Path -LiteralPath $installEsd -PathType Leaf)) {
$isBootWimInExtractedMedia = $true
}
}
if (-not $isBootWimInExtractedMedia) {
[void]$files.Add($file)
}
}
foreach ($file in $files) {
$kind = switch ($file.Extension.ToLowerInvariant()) {
'.iso' { 'Image ISO' }
'.wim' { 'Image WIM Windows' }
'.esd' { 'Image ESD Windows' }
}
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
"Image d’installation dans un support extrait"
} elseif ($file.Extension -ieq '.iso') {
"Support d’installation ISO"
} else {
"Image WIM/ESD autonome"
}
$results.Add([pscustomobject]@{
Type = $kind; Nom = $file.Name; Chemin = $file.FullName
Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail
})
}
}
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles."
}
Write-Log ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS'
return @($results | Sort-Object Type, Nom, Chemin -Unique)
}
function Get-WindowsImageEditions {
param([Parameter(Mandatory=$true)][string]$ImagePath)
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
throw "Fichier image introuvable : $ImagePath"
}
$normalized = @()
$getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue
if ($getWindowsImage) {
try {
$images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop)
foreach ($image in $images) {
$indexValue = $null
foreach ($propertyName in @('ImageIndex', 'Index')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$indexValue = [int]$image.$propertyName
break
}
}
if ($null -eq $indexValue) { continue }
$nameValue = $null
foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$nameValue = [string]$image.$propertyName
break
}
}
$archValue = $null
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$archValue = $image.$propertyName
break
}
}
$versionValue = $null
foreach ($propertyName in @('Version', 'ImageVersion')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$versionValue = [string]$image.$propertyName
break
}
}
$normalized += [pscustomobject]@{
Index = $indexValue
Name = $nameValue
Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null }
Architecture = $archValue
Version = $versionValue
Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null }
}
}
} catch {
Write-Log "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING'
}
}
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
# Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM.
if ($normalized.Count -eq 0) {
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")"
}
$current = $null
foreach ($line in $output) {
$lineText = [string]$line
if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') {
if ($null -ne $current) { $normalized += [pscustomobject]$current }
$current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null }
} elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') {
$current.Name = $Matches[1]
} elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') {
$current.Description = $Matches[1]
}
}
if ($null -ne $current) { $normalized += [pscustomobject]$current }
}
if ($normalized.Count -eq 0) {
throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath"
}
# Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms :
# selon la version du module, les propriétés Architecture/Version peuvent être absentes.
foreach ($edition in $normalized) {
if ($edition.Architecture -and $edition.Version) { continue }
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING'
continue
}
$insideImageDetails = $false
$imageVersion = $null
$servicePackBuild = $null
foreach ($detailLine in $detailOutput) {
$detailText = [string]$detailLine
if ($detailText -match '^\s*Details for image\s*:') {
$insideImageDetails = $true
continue
}
# Ignorer la version de DISM affichée au début de sa sortie.
if (-not $insideImageDetails) { continue }
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
$edition.Architecture = $Matches[1]
} elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') {
$imageVersion = $Matches[1]
} elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') {
$servicePackBuild = $Matches[1]
} elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') {
$edition.Name = $Matches[1]
} elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') {
$edition.Edition = $Matches[1]
}
}
if ($imageVersion) {
$edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') {
"$imageVersion.$servicePackBuild"
} else {
$imageVersion
}
}
}
# Version globale de l'image : ne l'afficher que si les métadonnées des index
# donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne.
$versionValues = @($normalized | ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
} | Select-Object -Unique)
$globalVersion = $null
if ($versionValues.Count -eq 1) {
$globalVersion = $versionValues[0]
}
foreach ($edition in $normalized) {
if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') {
switch ([string]$edition.Architecture) {
'0' { $edition.Architecture = 'x86' }
'5' { $edition.Architecture = 'ARM' }
'9' { $edition.Architecture = 'x64' }
'12' { $edition.Architecture = 'ARM64' }
}
}
}
return [pscustomobject]@{
Editions = @($normalized)
GlobalVersion = $globalVersion
}
}
function Resolve-InstallImagePath {
param([Parameter(Mandatory=$true)][string]$SelectedPath)
if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) {
return $SelectedPath
}
if (Test-Path -LiteralPath $SelectedPath -PathType Container) {
foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) {
$candidate = Join-Path $SelectedPath $relative
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath"
}
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd."
}
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -PassThru -ErrorAction Stop
try {
$volumes = @($diskImage | Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter })
foreach ($volume in $volumes) {
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
$candidate = "$($volume.DriveLetter):\$relative"
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
}
throw "L'ISO ne contient pas de fichier Sources\install.wim ou Sources\install.esd."
} finally {
Dismount-DiskImage -ImagePath $SelectedPath -ErrorAction SilentlyContinue
}
}
throw "Type de source non pris en charge pour l'analyse : $SelectedPath"
}
function Show-WindowsSourcesDialog {
$dlg = New-Object System.Windows.Forms.Form
$dlg.Text = 'Sources Windows - lecteur DeployCenter et partage SMB'
$dlg.StartPosition = 'CenterParent'
$dlg.Size = New-Object System.Drawing.Size(1100, 700)
$dlg.MinimumSize = New-Object System.Drawing.Size(700, 450)
Set-DeployCenterFormSizing -Form $dlg -Resizable
# Ouvrir directement en plein écran pour exploiter toute la surface disponible.
$dlg.WindowState = [System.Windows.Forms.FormWindowState]::Maximized
$dlg.ShowInTaskbar = $false
$dlg.BackColor = $script:Theme.Background
$dlg.ForeColor = $script:Theme.Text
$dlg.Font = $script:FontRegular
$lblPath = New-Object System.Windows.Forms.Label
$lblPath.Text = 'Chemins à analyser (séparés par ;) :'
$lblPath.Location = New-Object System.Drawing.Point(16, 16)
$lblPath.Size = New-Object System.Drawing.Size(260, 24)
$lblPath.ForeColor = $script:Theme.Text
$dlg.Controls.Add($lblPath)
$txtPath = New-Object System.Windows.Forms.TextBox
$txtPath.Location = New-Object System.Drawing.Point(16, 42)
$txtPath.Size = New-Object System.Drawing.Size(560, 26)
$txtPath.Anchor = 'Top,Left,Right'
$defaultSourceRoot = $script:DeployVolumeRoot
if ([string]::IsNullOrWhiteSpace($defaultSourceRoot)) { $defaultSourceRoot = $script:DeployRoot }
$configuredPaths = [string]$script:Config.SourcePaths
if ([string]::IsNullOrWhiteSpace($configuredPaths)) {
$txtPath.Text = $defaultSourceRoot
} else {
$txtPath.Text = $configuredPaths.Replace("%DEPLOY_VOLUME%", $defaultSourceRoot)
}
$txtPath.BackColor = $script:Theme.PanelAlt
$txtPath.ForeColor = $script:Theme.Text
$dlg.Controls.Add($txtPath)
$btnScan = New-Object System.Windows.Forms.Button
$btnScan.Text = 'Rechercher'
$btnScan.Location = New-Object System.Drawing.Point(650, 40)
$btnScan.Size = New-Object System.Drawing.Size(100, 30)
$btnScan.Anchor = 'Top,Right'
$btnScan.UseVisualStyleBackColor = $false
$btnScan.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$btnScan.FlatAppearance.BorderColor = $script:Theme.Border
$btnScan.BackColor = $script:Theme.Accent
$btnScan.ForeColor = [System.Drawing.Color]::White
$dlg.Controls.Add($btnScan)
$btnEditions = New-Object System.Windows.Forms.Button
$btnEditions.Text = 'Afficher les éditions'
$btnEditions.Location = New-Object System.Drawing.Point(760, 40)
$btnEditions.Size = New-Object System.Drawing.Size(150, 30)
$btnEditions.Anchor = 'Top,Right'
$btnEditions.UseVisualStyleBackColor = $false
$btnEditions.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$btnEditions.FlatAppearance.BorderColor = $script:Theme.Border
$btnEditions.BackColor = $script:Theme.Panel
$btnEditions.ForeColor = $script:Theme.Text
$dlg.Controls.Add($btnEditions)
$list = New-Object System.Windows.Forms.ListView
$list.Location = New-Object System.Drawing.Point(16, 84)
$list.Size = New-Object System.Drawing.Size(770, 420)
$list.Anchor = 'Top,Bottom,Left,Right'
$list.View = 'Details'; $list.FullRowSelect = $true; $list.GridLines = $true
$list.MultiSelect = $false
$list.HideSelection = $true
$list.BackColor = $script:Theme.PanelAlt
$list.ForeColor = $script:Theme.Text
$list.OwnerDraw = $true
[void]$list.Columns.Add('Type', 135)
[void]$list.Columns.Add('Nom', 260)
[void]$list.Columns.Add('Taille', 80)
[void]$list.Columns.Add('Chemin', 250)
# La colonne Nom est prioritaire : elle absorbe l'espace libre.
$resizeSourceColumns = {
if ($list.Columns.Count -ge 4) {
$availableWidth = $list.ClientSize.Width - $list.Columns[0].Width - $list.Columns[2].Width - $list.Columns[3].Width - 4
if ($list.Items.Count -gt 0 -and $list.Items.Count -gt [math]::Floor($list.ClientSize.Height / 20)) {
$availableWidth -= [System.Windows.Forms.SystemInformation]::VerticalScrollBarWidth
}
$list.Columns[1].Width = [Math]::Max(180, $availableWidth)
}
}
$list.Add_SizeChanged($resizeSourceColumns)
$list.Add_HandleCreated($resizeSourceColumns)
# Dessin personnalise des en-tetes et des lignes pour que le ListView
# respecte le theme actif au lieu d'utiliser les en-tetes natifs blancs.
$list.Add_DrawColumnHeader({
param($sender, $e)
$headerColor = if ($script:Theme.Contains('TableHeader')) { $script:Theme.TableHeader } else { $script:Theme.PanelAlt }
$headerBrush = New-Object System.Drawing.SolidBrush($headerColor)
$textBrush = New-Object System.Drawing.SolidBrush($script:Theme.Text)
$borderPen = New-Object System.Drawing.Pen($script:Theme.Border)
try {
$e.Graphics.FillRectangle($headerBrush, $e.Bounds)
$e.Graphics.DrawRectangle($borderPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
$flags = [System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.Header.Text, $list.Font, $e.Bounds, $script:Theme.Text, $flags)
} finally {
$headerBrush.Dispose(); $textBrush.Dispose(); $borderPen.Dispose()
}
})
$list.Add_DrawItem({
param($sender, $e)
if ($list.View -ne [System.Windows.Forms.View]::Details) {
$e.DrawDefault = $true
}
})
$list.Add_DrawSubItem({
param($sender, $e)
$rowBack = if (($e.Item.Index % 2) -eq 0) { $script:Theme.TableRowEven } else { $script:Theme.TableRowOdd }
$rowFore = $script:Theme.Text
# Ne surligner que l'element réellement sélectionné, jamais toutes les lignes.
if ($e.Item.Selected -and $list.Focused) {
$rowBack = $script:Theme.Accent
$rowFore = [System.Drawing.Color]::White
}
$rowBrush = New-Object System.Drawing.SolidBrush($rowBack)
$textBrush = New-Object System.Drawing.SolidBrush($rowFore)
$borderPen = New-Object System.Drawing.Pen($script:Theme.Border)
try {
$e.Graphics.FillRectangle($rowBrush, $e.Bounds)
$e.Graphics.DrawRectangle($borderPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
$flags = [System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis -bor [System.Windows.Forms.TextFormatFlags]::NoPrefix
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.SubItem.Text, $list.Font, $e.Bounds, $rowFore, $flags)
} finally {
$rowBrush.Dispose(); $textBrush.Dispose(); $borderPen.Dispose()
}
})
# Filtre local : il ne relance jamais l'analyse des lecteurs ou partages.
$lblFilter = New-Object System.Windows.Forms.Label
$lblFilter.Text = 'Filtrer la liste :'
$lblFilter.Location = New-Object System.Drawing.Point(16, 80)
$lblFilter.Size = New-Object System.Drawing.Size(100, 24)
$lblFilter.ForeColor = $script:Theme.Text
$dlg.Controls.Add($lblFilter)
$txtFilter = New-Object System.Windows.Forms.TextBox
$txtFilter.Location = New-Object System.Drawing.Point(120, 78)
$txtFilter.Size = New-Object System.Drawing.Size(500, 26)
$txtFilter.Anchor = 'Top,Left,Right'
$txtFilter.BackColor = $script:Theme.PanelAlt
$txtFilter.ForeColor = $script:Theme.Text
$dlg.Controls.Add($txtFilter)
$list.Location = New-Object System.Drawing.Point(16, 112)
$dlg.Controls.Add($list)
# Etat partage explicite entre les gestionnaires d'evenements WinForms.
# En PowerShell, les callbacks peuvent s'executer dans une portee differente :
# une variable locale $allFound peut alors rester vide dans le filtre.
$sourceState = [hashtable]::Synchronized(@{ AllFound = @() })
$renderSourceList = {
param([string]$Query)
$previousSelection = $null
if ($list.SelectedItems.Count -gt 0) {
$previousSelection = [string]$list.SelectedItems[0].SubItems[3].Text
}
$needle = ([string]$Query).Trim()
$visibleEntries = New-Object System.Collections.Generic.List[object]
$entries = @($sourceState.AllFound)
foreach ($entry in $entries) {
if ([string]::IsNullOrWhiteSpace($needle)) {
[void]$visibleEntries.Add($entry)
continue
}
$searchable = '{0} {1} {2} {3} {4}' -f `
[string]$entry.Type, [string]$entry.Nom, [string]$entry.Chemin, `
[string]$entry.Taille, [string]$entry.Detail
if ($searchable.IndexOf($needle, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
[void]$visibleEntries.Add($entry)
}
}
$list.BeginUpdate()
try {
$list.Items.Clear()
foreach ($entry in $visibleEntries) {
$item = New-Object System.Windows.Forms.ListViewItem([string]$entry.Type)
[void]$item.SubItems.Add([string]$entry.Nom)
[void]$item.SubItems.Add([string]$entry.Taille)
[void]$item.SubItems.Add([string]$entry.Chemin)
if ($previousSelection -and ([string]$entry.Chemin -eq $previousSelection)) {
$item.Selected = $true
}
[void]$list.Items.Add($item)
}
} finally {
$list.EndUpdate()
}
if ($entries.Count -gt 0) {
$lblStatus.Text = ('{0} résultat(s) affiché(s) sur {1}. Filtre instantané ; aucun fichier modifié.' -f $list.Items.Count, $entries.Count)
} elseif ([string]::IsNullOrWhiteSpace($needle)) {
$lblStatus.Text = 'Aucune source ISO/WIM/ESD ou installation extraite detectee dans les chemins accessibles.'
} else {
$lblStatus.Text = ('Aucun resultat pour « {0} ». Efface le filtre pour revoir la liste.' -f $needle)
}
}
$txtFilter.Add_TextChanged({ & $renderSourceList $txtFilter.Text })
$lblStatus = New-Object System.Windows.Forms.Label
$lblStatus.Text = 'Analyse en lecture seule du lecteur de DeployCenter et/ou du partage SMB. Plusieurs chemins peuvent etre separes par un point-virgule.'
$lblStatus.Location = New-Object System.Drawing.Point(16, 515)
$lblStatus.Size = New-Object System.Drawing.Size(770, 36)
$lblStatus.Anchor = 'Bottom,Left,Right'
$lblStatus.AutoSize = $false
$lblStatus.ForeColor = $script:Theme.Secondary
$dlg.Controls.Add($lblStatus)
# Recalculer les positions depuis la taille réelle de la fenêtre :
# le tableau occupe l'espace restant et le statut reste toujours visible en bas.
$resizeLayout = {
$clientWidth = $dlg.ClientSize.Width
$clientHeight = $dlg.ClientSize.Height
$rightMargin = 16
$gap = 12
$btnWidth = 88
$editionsWidth = 148
$left = 12
$right = [Math]::Max($left + $btnWidth, $clientWidth - $rightMargin)
$btnEditionsLeft = [Math]::Max($left, $right - $editionsWidth)
$btnScanLeft = [Math]::Max($left, $btnEditionsLeft - $gap - $btnWidth)
$txtPath.Width = [Math]::Max(100, $btnScanLeft - $left - $gap)
$btnEditions.Location = New-Object System.Drawing.Point($btnEditionsLeft, 40)
$btnEditions.Width = [Math]::Max(120, $right - $btnEditionsLeft)
$btnScan.Location = New-Object System.Drawing.Point($btnScanLeft, 40)
$btnScan.Width = [Math]::Max(80, $btnEditionsLeft - $gap - $btnScanLeft)
$btnScan.Anchor = 'Top,Right'
$btnEditions.Anchor = 'Top,Right'
$txtPath.Anchor = 'Top,Left,Right'
$lblFilter.Location = New-Object System.Drawing.Point(16, 80)
$txtFilter.Location = New-Object System.Drawing.Point(120, 78)
$txtFilter.Size = New-Object System.Drawing.Size([Math]::Max(180, $clientWidth - 152), 26)
$list.Location = New-Object System.Drawing.Point(16, 112)
$list.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), [Math]::Max(120, $clientHeight - 205))
$list.Anchor = 'Top,Bottom,Left,Right'
$lblStatus.Location = New-Object System.Drawing.Point(16, ($clientHeight - 40))
$lblStatus.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), 32)
$lblStatus.Anchor = 'Bottom,Left,Right'
}
$dlg.Add_Shown({
& $resizeLayout
# Lancer automatiquement la première analyse à l'ouverture.
# Le bouton Rechercher reste disponible si l'utilisateur change les chemins.
$btnScan.PerformClick()
})
$dlg.Add_SizeChanged({ & $resizeLayout })
$btnEditions.Add_Click({
if ($list.SelectedItems.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Sélectionne d''abord une source dans la liste.', 'Éditions Windows', 'OK', 'Information') | Out-Null
return
}
$selectedPath = [string]$list.SelectedItems[0].SubItems[3].Text
$selectedName = [string]$list.SelectedItems[0].SubItems[1].Text
$btnEditions.Enabled = $false
$btnScan.Enabled = $false
$dlg.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
$lblStatus.Text = "Analyse des editions de $selectedName en cours..."
try {
$imagePath = Resolve-InstallImagePath -SelectedPath $selectedPath
$editionResult = Get-WindowsImageEditions -ImagePath $imagePath
$editions = @($editionResult.Editions)
$globalImageVersion = [string]$editionResult.GlobalVersion
$lines = foreach ($edition in $editions) {
$index = if ($edition.ImageIndex) { $edition.ImageIndex } elseif ($edition.Index) { $edition.Index } else { '?' }
$name = if ($edition.ImageName) { $edition.ImageName } elseif ($edition.Name) { $edition.Name } else { 'Nom non disponible' }
$arch = if ($edition.Architecture) { [string]$edition.Architecture } else { 'Architecture non disponible' }
$version = if ($edition.Version) { [string]$edition.Version } else { 'Version non disponible' }
"Index : $index`r`nNom : $name`r`nArchitecture : $arch`r`nVersion : $version"
}
# Le formulaire enfant est modal : actualiser le statut avant son affichage,
# sinon le formulaire parent reste visuellement sur « analyse en cours ».
$lblStatus.Text = "$($editions.Count) édition(s) détectée(s) dans $selectedName. Sélectionne une édition."
$dlg.Refresh()
# Tableau des editions ; l'index DISM selectionne est conserve pour la suite.
$resultForm = New-Object System.Windows.Forms.Form
$resultForm.Text = 'Éditions Windows disponibles'
$resultForm.StartPosition = 'CenterParent'
$resultForm.Size = New-Object System.Drawing.Size(700, 480)
$resultForm.MinimumSize = New-Object System.Drawing.Size(520, 320)
Set-DeployCenterFormSizing -Form $resultForm
$resultForm.ShowInTaskbar = $false
$resultForm.BackColor = $script:Theme.Background
$resultForm.ForeColor = $script:Theme.Text
$sourceLabel = New-Object System.Windows.Forms.Label
$sourceLabel.Text = "Source : $selectedPath"
$sourceLabel.Dock = [System.Windows.Forms.DockStyle]::Top
$sourceLabel.Height = 38
$sourceLabel.Padding = New-Object System.Windows.Forms.Padding(10, 8, 10, 4)
$sourceLabel.AutoEllipsis = $true
$sourceLabel.BackColor = $script:Theme.Background
$sourceLabel.ForeColor = $script:Theme.Text
$editionList = New-Object System.Windows.Forms.ListView
$editionList.View = [System.Windows.Forms.View]::Details
$editionList.FullRowSelect = $true
$editionList.GridLines = $true
$editionList.MultiSelect = $false
$editionList.HideSelection = $false
# Une seule édition peut être sélectionnée à la fois.
$editionList.Sorting = [System.Windows.Forms.SortOrder]::None
$editionList.Dock = [System.Windows.Forms.DockStyle]::Fill
$editionList.BackColor = $script:Theme.Panel
$editionList.ForeColor = $script:Theme.Text
[void]$editionList.Columns.Add('Index', 65)
[void]$editionList.Columns.Add('Édition', 260)
[void]$editionList.Columns.Add('Architecture', 100)
[void]$editionList.Columns.Add('Version', 105)
# Édition est la colonne prioritaire : Version conserve une largeur fixe.
$resizeEditionColumns = {
if ($editionList.Columns.Count -ge 4) {
$availableWidth = $editionList.ClientSize.Width - $editionList.Columns[0].Width - $editionList.Columns[2].Width - $editionList.Columns[3].Width - 4
if ($editionList.Items.Count -gt 0 -and $editionList.Items.Count -gt [Math]::Floor($editionList.ClientSize.Height / 20)) {
$availableWidth -= [System.Windows.Forms.SystemInformation]::VerticalScrollBarWidth
}
$editionList.Columns[1].Width = [Math]::Max(220, $availableWidth)
}
}
$editionList.Add_SizeChanged($resizeEditionColumns)
$resultForm.Add_Shown($resizeEditionColumns)
# Dessin personnalise des en-tetes et cellules pour que ListView
# respecte le theme Light/Dark au lieu de garder un en-tete blanc.
$editionList.OwnerDraw = $true
$editionList.Add_DrawColumnHeader({
param($sender, $e)
$headerColor = if ($script:Theme.Contains('TableHeader')) { $script:Theme.TableHeader } else { $script:Theme.PanelAlt }
$headerBrush = New-Object System.Drawing.SolidBrush($headerColor)
$headerPen = New-Object System.Drawing.Pen($script:Theme.Border)
try {
$e.Graphics.FillRectangle($headerBrush, $e.Bounds)
$e.Graphics.DrawRectangle($headerPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
[System.Windows.Forms.TextRenderer]::DrawText(
$e.Graphics, $e.Header.Text, $sender.Font, $e.Bounds,
$script:Theme.Text,
([System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor
[System.Windows.Forms.TextFormatFlags]::Left -bor
[System.Windows.Forms.TextFormatFlags]::EndEllipsis)
)
} finally {
$headerBrush.Dispose()
$headerPen.Dispose()
}
})
$editionList.Add_DrawItem({ param($sender, $e) })
$editionList.Add_DrawSubItem({
param($sender, $e)
$cellBack = if (($e.Item.Index % 2) -eq 0) { $script:Theme.TableRowEven } else { $script:Theme.TableRowOdd }
$cellFore = $script:Theme.Text
if ($e.Item.Selected) {
$cellBack = $script:Theme.Accent
$cellFore = [System.Drawing.Color]::White
}
$cellBrush = New-Object System.Drawing.SolidBrush($cellBack)
$cellPen = New-Object System.Drawing.Pen($script:Theme.Border)
try {
$e.Graphics.FillRectangle($cellBrush, $e.Bounds)
if ($sender.GridLines) {
$e.Graphics.DrawRectangle($cellPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
}
[System.Windows.Forms.TextRenderer]::DrawText(
$e.Graphics, $e.SubItem.Text, $sender.Font, $e.Bounds,
$cellFore,
([System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor
[System.Windows.Forms.TextFormatFlags]::Left -bor
[System.Windows.Forms.TextFormatFlags]::EndEllipsis)
)
} finally {
$cellBrush.Dispose()
$cellPen.Dispose()
}
})
foreach ($edition in $editions) {
$index = if ($edition.ImageIndex) { [string]$edition.ImageIndex } elseif ($edition.Index) { [string]$edition.Index } else { '?' }
$name = if ($edition.ImageName) { [string]$edition.ImageName } elseif ($edition.Name) { [string]$edition.Name } elseif ($edition.ImageDescription) { [string]$edition.ImageDescription } else { 'Nom non disponible' }
$archValue = $null
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
if ($edition.PSObject.Properties[$propertyName] -and $edition.$propertyName) {
$archValue = $edition.$propertyName
break
}
}
$arch = if ($null -ne $archValue) {
switch ([string]$archValue) {
'0' { 'x86' }
'5' { 'ARM' }
'9' { 'x64' }
'12' { 'ARM64' }
default { [string]$archValue }
}
} else { 'Non disponible' }
$version = if ($edition.Version) { [string]$edition.Version } else { 'Non disponible' }
$row = New-Object System.Windows.Forms.ListViewItem($index)
[void]$row.SubItems.Add($name)
[void]$row.SubItems.Add($arch)
[void]$row.SubItems.Add($version)
$row.Tag = $edition
[void]$editionList.Items.Add($row)
}
$buttonPanel = New-Object System.Windows.Forms.Panel
$buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Bottom
$buttonPanel.Height = 52
$buttonPanel.BackColor = $script:Theme.Background
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Text = 'Annuler'
$cancelButton.Size = New-Object System.Drawing.Size(100, 30)
$cancelButton.Anchor = 'Right,Top'
$cancelButton.Location = New-Object System.Drawing.Point(0, 10)
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$validateButton = New-Object System.Windows.Forms.Button
$validateButton.Text = 'Valider l''édition'
$validateButton.Size = New-Object System.Drawing.Size(140, 30)
$validateButton.Anchor = 'Right,Top'
$validateButton.Location = New-Object System.Drawing.Point(0, 10)
$validateButton.Enabled = $false
$editionList.Add_SelectedIndexChanged({
$validateButton.Enabled = ($editionList.SelectedItems.Count -gt 0 -and $editionList.SelectedItems[0].SubItems[0].Text -ne '?')
})
$validateButton.Add_Click({
if ($editionList.SelectedItems.Count -gt 0) {
$resultForm.Tag = [pscustomobject]@{
SourcePath = $selectedPath
ImagePath = $imagePath
Edition = $editionList.SelectedItems[0].Tag
Index = [int]$editionList.SelectedItems[0].SubItems[0].Text
Name = $editionList.SelectedItems[0].SubItems[1].Text
}
$resultForm.DialogResult = [System.Windows.Forms.DialogResult]::OK
$resultForm.Close()
}
})
$editionList.Add_DoubleClick({
if ($editionList.SelectedItems.Count -gt 0 -and $validateButton.Enabled) {
$validateButton.PerformClick()
}
})
$buttonPanel.Controls.Add($cancelButton)
$buttonPanel.Controls.Add($validateButton)
$buttonPanel.Add_Resize({
$validateButton.Location = New-Object System.Drawing.Point(($buttonPanel.ClientSize.Width - $validateButton.Width - 12), 10)
$cancelButton.Location = New-Object System.Drawing.Point(($validateButton.Left - $cancelButton.Width - 8), 10)
})
$resultForm.Controls.Add($editionList)
$resultForm.Controls.Add($buttonPanel)
$resultForm.Controls.Add($sourceLabel)
$resultForm.AcceptButton = $validateButton
$resultForm.CancelButton = $cancelButton
Add-SessionFooter -TargetForm $resultForm -ShowFooter $false
$dialogResult = $resultForm.ShowDialog($dlg)
$chosenEdition = $resultForm.Tag
$resultForm.Dispose()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK -and $chosenEdition) {
$script:SelectedWindowsEdition = $chosenEdition
$lblStatus.Text = "Édition sélectionnée : $($chosenEdition.Name) (index $($chosenEdition.Index))."
Write-Log "Edition selectionnee : $($chosenEdition.Name), index $($chosenEdition.Index), image $imagePath" 'SUCCESS'
} else {
$lblStatus.Text = "$($editions.Count) édition(s) trouvée(s) dans $selectedName. Aucune nouvelle édition sélectionnée."
}
Write-Log "Analyse des editions terminee : $imagePath ($($editions.Count) edition(s))." 'SUCCESS'
} catch {
$lblStatus.Text = "Echec de l'analyse des editions de $selectedName."
Write-Log "Erreur analyse editions $selectedPath : $($_.Exception.Message)" 'ERROR'
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Analyse des editions', 'OK', 'Warning') | Out-Null
} finally {
$dlg.Cursor = [System.Windows.Forms.Cursors]::Default
$btnEditions.Enabled = $true
$btnScan.Enabled = $true
}
})
$btnScan.Add_Click({
$btnScan.Enabled = $false
$dlg.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
$lblStatus.Text = 'Recherche en cours sur les chemins indiques...'
$sourceState.AllFound = @()
$list.Items.Clear()
try {
# La recherche ne modifie jamais DeployCenter.ini.
$sourceState.AllFound = @(Find-WindowsSources -RootPath $txtPath.Text)
& $renderSourceList $txtFilter.Text
if (@($sourceState.AllFound).Count -eq 0) {
$lblStatus.Text = 'Aucune source ISO/WIM/ESD ou installation extraite detectee dans les chemins accessibles.'
Write-Log 'Aucune source Windows detectee dans les chemins accessibles.' 'WARNING'
}
} catch {
$lblStatus.Text = 'Echec de la recherche. Consulte le message.'
Write-Log "Erreur detection sources : $($_.Exception.Message)" 'ERROR'
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Detection des sources', 'OK', 'Warning') | Out-Null
} finally {
$dlg.Cursor = [System.Windows.Forms.Cursors]::Default
$btnScan.Enabled = $true
}
})
Add-SessionFooter -TargetForm $dlg
[void]$dlg.ShowDialog($script:form)
$dlg.Dispose()
}
# ============================================================
# INVENTAIRE MATERIEL
# ============================================================
function Update-HardwareView {
$script:btnRefresh.Enabled = $false
$script:form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
try {
if (-not (Test-Path $script:HardwareScript)) {
throw "Fichier HardwareInfo.ps1 introuvable : $script:HardwareScript"
}
. $script:HardwareScript
$info = Get-HardwareInfo
$script:lblManufacturer.Text = "Fabricant : $($info.Manufacturer)"
$script:lblModel.Text = "Modèle : $($info.Model)"
$script:lblSerial.Text = "Numéro de série : $($info.SerialNumber)"
$script:lblFirmware.Text = "Firmware : $($info.Firmware)"
$script:lblArchitecture.Text = "Architecture : $($info.Architecture)"
if ($info.WinPE) {
$script:lblWinPE.Text = "Environnement : WinPE"
$script:lblWinPE.ForeColor = $script:Theme.Success
}
else {
$script:lblWinPE.Text = "Environnement : Windows classique"
$script:lblWinPE.ForeColor = $script:Theme.Secondary
}
$previousDiskIndex = $null
if ($null -ne $script:cmbDisk.SelectedItem -and $null -ne $script:cmbDisk.SelectedItem.Tag) {
$previousDiskIndex = [string]$script:cmbDisk.SelectedItem.Tag
}
$script:cmbDisk.BeginUpdate()
try {
$script:cmbDisk.Items.Clear()
foreach ($disk in $info.Disks) {
$diskLabel = "Disque {0} - {1} - {2:N1} Go - {3} - {4}" -f $disk.Index, $disk.Model, $disk.SizeGB, $disk.Interface, $disk.Status
$diskChoice = [pscustomobject]@{ Text = $diskLabel; Tag = [string]$disk.Index }
[void]$script:cmbDisk.Items.Add($diskChoice)
}
$script:cmbDisk.DisplayMember = 'Text'
if ($script:cmbDisk.Items.Count -gt 0) {
$selectedIndex = -1
if ($null -ne $previousDiskIndex) {
for ($i = 0; $i -lt $script:cmbDisk.Items.Count; $i++) {
if ([string]$script:cmbDisk.Items[$i].Tag -eq $previousDiskIndex) { $selectedIndex = $i; break }
}
}
if ($selectedIndex -lt 0 -and $script:cmbDisk.Items.Count -eq 1) { $selectedIndex = 0 }
$script:cmbDisk.SelectedIndex = $selectedIndex
}
}
finally {
$script:cmbDisk.EndUpdate()
}
$json = $info | ConvertTo-Json -Depth 6
Set-Content -LiteralPath $script:HardwareJson -Value $json -Encoding UTF8
Write-DeployInventory -HardwareInfo $info
Write-Log "Inventaire materiel actualise." "SUCCESS"
foreach ($errorMessage in $info.Errors) {
Write-Log $errorMessage "WARNING"
}
if ($info.Disks.Count -eq 0) {
Write-Log "Aucun disque physique detecte." "WARNING"
}
}
catch {
Write-Log "Erreur inventaire : $($_.Exception.Message)" "ERROR"
[System.Windows.Forms.MessageBox]::Show(
$_.Exception.Message,
"Erreur DeployCenter",
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Error
) | Out-Null
}
finally {
$script:btnRefresh.Enabled = $true
$script:form.Cursor = [System.Windows.Forms.Cursors]::Default
}
}
# ============================================================
# MISE EN PAGE ADAPTATIVE
# Les grandes zones suivent la fenetre ; les marges et hauteurs de controles restent fixes.
# ============================================================
function Update-DeployCenterLayout {
if ($null -eq $script:form -or $script:form.IsDisposed) { return }
$w = $script:form.ClientSize.Width
$h = $script:form.ClientSize.Height
$m = 24
$contentWidth = [Math]::Max(300, $w - (2 * $m))
# En-tete : le selecteur de theme reste accroche au bord droit.
if ($null -ne $script:cmbTheme) {
$script:cmbTheme.Location = New-Object System.Drawing.Point([Math]::Max($m, $w - $m - $script:cmbTheme.Width), 25)
}
# Les deux panneaux superieurs conservent leur hauteur, mais s'etirent en largeur.
$script:groupPC.Location = New-Object System.Drawing.Point($m, 100)
$script:groupPC.Size = New-Object System.Drawing.Size($contentWidth, 105)
$script:groupDisks.Location = New-Object System.Drawing.Point($m, 220)
$script:groupDisks.Size = New-Object System.Drawing.Size($contentWidth, 78)
Set-DiskSelectorWidth
# Le journal absorbe l'espace vertical restant au-dessus des actions et du pied de page.
$footerHeight = 26
$buttonHeightLocal = 38
$buttonYLocal = $h - $footerHeight - 14 - $buttonHeightLocal
$logYLocal = $script:groupDisks.Bottom + 14
$logHeightLocal = [Math]::Max(90, $buttonYLocal - $logYLocal - 12)
$script:groupLog.Location = New-Object System.Drawing.Point($m, $logYLocal)
$script:groupLog.Size = New-Object System.Drawing.Size($contentWidth, $logHeightLocal)
$script:txtLog.Location = New-Object System.Drawing.Point(12, 28)
$script:txtLog.Size = New-Object System.Drawing.Size([Math]::Max(250, $script:groupLog.ClientSize.Width - 24), [Math]::Max(50, $script:groupLog.ClientSize.Height - 40))
$buttonWidthLocal = 140
$buttonGapLocal = 12
$script:btnClose.Size = New-Object System.Drawing.Size($buttonWidthLocal, $buttonHeightLocal)
$script:btnRefresh.Size = New-Object System.Drawing.Size($buttonWidthLocal, $buttonHeightLocal)
$script:btnSources.Size = New-Object System.Drawing.Size(170, $buttonHeightLocal)
$script:btnClose.Location = New-Object System.Drawing.Point([Math]::Max($m, $w - $m - $buttonWidthLocal), $buttonYLocal)
$script:btnRefresh.Location = New-Object System.Drawing.Point([Math]::Max($m, $script:btnClose.Left - $buttonGapLocal - $buttonWidthLocal), $buttonYLocal)
$script:btnSources.Location = New-Object System.Drawing.Point([Math]::Max($m, $script:btnRefresh.Left - $buttonGapLocal - 170), $buttonYLocal)
Set-DiskSelectorWidth
}
# ============================================================
# EVENEMENTS
# ============================================================
$script:btnSources.Add_Click({
Show-WindowsSourcesDialog
})
$script:btnRefresh.Add_Click({
Update-HardwareView
})
$script:btnClose.Add_Click({
$script:form.Close()
})
$script:cmbTheme.Add_SelectedIndexChanged({
if ($script:cmbTheme.SelectedItem) {
Set-DeployCenterTheme -ThemeName ([string]$script:cmbTheme.SelectedItem)
}
})
$script:cmbDisk.Add_SizeChanged({
Set-DiskSelectorWidth
})
$script:form.Add_SizeChanged({
Update-DeployCenterLayout
})
$script:form.Add_KeyDown({
param($sender, $eventArgs)
if ($eventArgs.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
$script:form.Close()
}
elseif ($eventArgs.KeyCode -eq [System.Windows.Forms.Keys]::F5) {
Update-HardwareView
}
})
$script:form.Add_Shown({
# Use the real maximized client width to position the theme button.
Update-DeployCenterLayout
$script:cmbTheme.BringToFront()
Write-Log "DeployCenter demarre."
Write-Log "Dossier : $script:DeployRoot"
Write-Log "Journal : $script:LogFile"
Update-HardwareView
})
$script:form.Add_FormClosed({
if ($null -ne $script:pictureLogo -and $null -ne $script:pictureLogo.Image) {
$script:pictureLogo.Image.Dispose()
}
$script:FontRegular.Dispose()
$script:FontSmall.Dispose()
$script:FontTitle.Dispose()
$script:FontSubtitle.Dispose()
$script:FontGroup.Dispose()
})
# ============================================================
# LANCEMENT
# ============================================================
try {
[void]$script:form.ShowDialog()
}
finally {
if ($script:EnableVnc -and (Get-Command Stop-DeployCenterVnc -ErrorAction SilentlyContinue)) {
Stop-DeployCenterVnc
}
$script:form.Dispose()
}