# 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 # Les profils de partitionnement sont des fichiers de données PSD1, validés par leur module. $partitioningModulePath = Join-Path $script:DeployRoot 'Modules\Partitioning.psm1' if (-not (Test-Path -LiteralPath $partitioningModulePath -PathType Leaf)) { throw "Module de partitionnement introuvable : $partitioningModulePath" } Import-Module $partitioningModulePath -Force $script:PartitionProfilesRoot = Join-Path $script:DeployRoot 'Partitioning\Profiles' $script:PartitionProfiles = @() foreach ($profileName in (Get-DeployCenterPartitionProfileNames -ProfilesRoot $script:PartitionProfilesRoot)) { try { $script:PartitionProfiles += Import-DeployCenterPartitionProfile -ProfilesRoot $script:PartitionProfilesRoot -ProfileName $profileName } catch { Write-Log "Profil de partitionnement '$profileName' ignoré : $($_.Exception.Message)" 'WARNING' } } if ($script:PartitionProfiles.Count -eq 0) { throw "Aucun profil de partitionnement valide trouvé dans $script:PartitionProfilesRoot" } $script:SelectedPartitionProfileIndex = 0 for ($profileIndex = 0; $profileIndex -lt $script:PartitionProfiles.Count; $profileIndex++) { if ($script:PartitionProfiles[$profileIndex].Default -eq $true) { $script:SelectedPartitionProfileIndex = $profileIndex break } } $script:SelectedPartitionProfile = $script:PartitionProfiles[$script:SelectedPartitionProfileIndex] $script:SelectedPartitionSizing = $null $script:SelectedDiskIndex = $null $script:SelectedWindowsEdition = $null $script:RefreshingDiskList = $false $script:ChangingPartitionProfileSelection = $false # 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 # Nettoyer les anciens exports séparés après l'écriture de l'inventaire consolidé. $legacyInventoryFiles = @( (Join-Path $env:TEMP 'HardwareInfo.json'), (Join-Path $script:InventoryDir 'hardware.json') ) foreach ($legacyFile in $legacyInventoryFiles) { if (Test-Path -LiteralPath $legacyFile -PathType Leaf) { try { Remove-Item -LiteralPath $legacyFile -Force -ErrorAction Stop } catch { Write-Log "Impossible de supprimer l'ancien export d'inventaire '$legacyFile' : $($_.Exception.Message)" 'WARNING' } } } } 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\.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) # ============================================================ # PREPARATION DU DEPLOIEMENT # ============================================================ $script:groupDisks = New-GroupBox -Text "Préparation du déploiement" ` -X $margin -Y 220 -Width $usableWidth -Height 226 $deploymentControls = New-DeployCenterDeploymentControls ` -Parent $script:groupDisks ` -ProfileNames @($script:PartitionProfiles | ForEach-Object { [string]$_.Name }) ` -Theme $script:Theme ` -Font $script:FontSmall $script:cmbDisk = $deploymentControls.DiskComboBox $script:btnChooseWindows = $deploymentControls.ChooseSystemButton $script:lblSelectedWindows = $deploymentControls.SelectedSystemLabel $script:lblPartitionProfile = $deploymentControls.PartitionProfileLabel $script:cmbPartitionProfile = $deploymentControls.PartitionProfileComboBox $script:txtHostName = $deploymentControls.HostNameTextBox $script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex $script:form.Controls.Add($script:groupDisks) function Set-DiskSelectorWidth { if ($null -eq $script:cmbDisk -or $null -eq $script:groupDisks) { return } Set-DeployCenterDeploymentControlsLayout -Parent $script:groupDisks -Controls $deploymentControls } # ============================================================ # JOURNAL DANS UNE FENETRE DEDIEE # ============================================================ $logWindow = New-DeployCenterLogWindow -Theme $script:Theme -Font $script:FontRegular $script:logForm = $logWindow.Form $script:txtLog = $logWindow.TextBox $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 } # BOUTONS JOURNAL, ACTUALISER ET FERMER # ============================================================ $buttonWidth = 140 $buttonGap = 12 $closeX = $screenWidth - $margin - $buttonWidth $refreshX = $closeX - $buttonGap - $buttonWidth $script:btnLog = New-Button -Text "Journal" ` -X $margin -Y $buttonY -Width $buttonWidth -Height $buttonHeight ` -Role "Button" $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:btnLog) $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 if ($Control.Name -eq 'lblSelectedWindows') { $Control.ForeColor = if ($script:SelectedWindowsEdition) { $script:Theme.Success } else { $script:Theme.Secondary } } # 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.ComboBox]) { Set-DeployCenterComboBoxTheme -ComboBox $Control -Theme $script:Theme } 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 Set-ControlTheme -Control $script:logForm # 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:btnChooseWindows -Theme $script:Theme -Role 'Button' Set-DeployCenterButtonStyle -Button $script:btnLog -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: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 - 52)) $lblStatus.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), 24) $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) { Show-DeployCenterMessage -Message 'Sélectionne d''abord une source dans la liste.' -Title 'Éditions Windows' -Icon Information -Owner $dlg -Theme $script:Theme -Font $script:FontRegular 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' $dlg.DialogResult = [System.Windows.Forms.DialogResult]::OK $dlg.Close() } 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' Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Analyse des éditions' -Icon Warning -Owner $dlg -Theme $script:Theme -Font $script:FontRegular } finally { $dlg.Cursor = [System.Windows.Forms.Cursors]::Default $btnEditions.Enabled = $true $btnScan.Enabled = $true } }) $list.Add_DoubleClick({ if ($list.SelectedItems.Count -gt 0 -and $btnEditions.Enabled) { $btnEditions.PerformClick() } }) $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' Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Détection des sources' -Icon Warning -Owner $dlg -Theme $script:Theme -Font $script:FontRegular } 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 } $script:HardwareInventory = $info $previousDiskIndex = $null if ($null -ne $script:cmbDisk.SelectedItem -and $null -ne $script:cmbDisk.SelectedItem.Tag) { $previousDiskIndex = [string]$script:cmbDisk.SelectedItem.Tag } $script:RefreshingDiskList = $true $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.BusType, $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() $script:RefreshingDiskList = $false } $script:SelectedDiskIndex = if ($script:cmbDisk.SelectedIndex -ge 0) { [string]$script:cmbDisk.SelectedItem.Tag } else { $null } 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" Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Erreur DeployCenter' -Icon Error -Owner $script:form -Theme $script:Theme -Font $script:FontRegular } finally { $script:btnRefresh.Enabled = $true $script:form.Cursor = [System.Windows.Forms.Cursors]::Default } } function Get-SelectedDeployDiskInfo { if ($null -eq $script:cmbDisk.SelectedItem -or $null -eq $script:HardwareInventory) { return $null } $diskIndex = [int]$script:cmbDisk.SelectedItem.Tag $matches = @($script:HardwareInventory.Disks | Where-Object { [int]$_.Index -eq $diskIndex } | Select-Object -First 1) if ($matches.Count -gt 0) { return $matches[0] } return $null } function Get-PartitionProfileSizingSelection { param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)]$DiskInfo, [System.Collections.IDictionary]$InitialValues = @{} ) $capacity = Get-DeployCenterPartitionSizingCapacity -Profile $Profile -DiskSizeBytes ([long]$DiskInfo.SizeBytes) return Show-DeployCenterPartitionSizingDialog ` -Profile $Profile ` -AvailableGB $capacity.UsableGB ` -Theme $script:Theme ` -Font $script:FontRegular ` -Owner $script:form ` -InitialValues $InitialValues } function Set-PartitionProfileSizingSelection { param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)]$DiskInfo, [System.Collections.IDictionary]$InitialValues = @{} ) $selection = Get-PartitionProfileSizingSelection -Profile $Profile -DiskInfo $DiskInfo -InitialValues $InitialValues if ($null -eq $selection) { return $false } $script:SelectedPartitionSizing = [pscustomobject]@{ ProfileId = [string]$Profile.Id DiskIndex = [int]$DiskInfo.Index Inputs = $selection.Inputs RemainderRole = $selection.RemainderRole RemainderGB = [double]$selection.RemainderGB AvailableGB = [double]$selection.AvailableGB } return $true } # ============================================================ # 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, 226) Set-DiskSelectorWidth $footerHeight = 26 $buttonHeightLocal = 38 $buttonYLocal = $h - $footerHeight - 14 - $buttonHeightLocal $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:btnLog.Size = New-Object System.Drawing.Size($buttonWidthLocal, $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:btnLog.Location = New-Object System.Drawing.Point($m, $buttonYLocal) Set-DiskSelectorWidth } # ============================================================ # EVENEMENTS # ============================================================ $script:btnChooseWindows.Add_Click({ Show-WindowsSourcesDialog if ($script:SelectedWindowsEdition) { $edition = $script:SelectedWindowsEdition $sourceDescription = if ($edition.ImagePath) { [string]$edition.ImagePath } else { [string]$edition.SourcePath } $script:lblSelectedWindows.Text = "{0} (index {1}) — {2}" -f $edition.Name, $edition.Index, $sourceDescription $script:lblSelectedWindows.ForeColor = $script:Theme.Success $script:lblSelectedWindows.Name = 'lblSelectedWindows' $script:cmbPartitionProfile.Enabled = $true } }) $script:btnLog.Add_Click({ if ($script:logForm.IsDisposed) { return } if (-not $script:logForm.Visible) { $script:logForm.Show($script:form) } $script:logForm.BringToFront() }) $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:cmbPartitionProfile.Add_SelectedIndexChanged({ if ($script:ChangingPartitionProfileSelection) { return } $selectedIndex = $script:cmbPartitionProfile.SelectedIndex if ($selectedIndex -ge 0 -and $selectedIndex -lt $script:PartitionProfiles.Count) { $candidateProfile = $script:PartitionProfiles[$selectedIndex] $sizeInputs = @($candidateProfile.Partitions | Where-Object { $_.Size.Policy -eq 'UserInputGB' }) if ($sizeInputs.Count -gt 0) { $diskInfo = Get-SelectedDeployDiskInfo $initialValues = @{} if ($script:SelectedPartitionSizing -and $script:SelectedPartitionSizing.ProfileId -eq $candidateProfile.Id) { $initialValues = $script:SelectedPartitionSizing.Inputs } if ($null -eq $diskInfo) { Show-DeployCenterMessage -Message "Sélectionne d’abord un disque physique." -Title 'Partitionnement' -Icon Information -Owner $script:form -Theme $script:Theme -Font $script:FontRegular } if ($null -eq $diskInfo -or -not (Set-PartitionProfileSizingSelection -Profile $candidateProfile -DiskInfo $diskInfo -InitialValues $initialValues)) { $script:ChangingPartitionProfileSelection = $true try { $script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex } finally { $script:ChangingPartitionProfileSelection = $false } return } } else { $script:SelectedPartitionSizing = $null } $script:SelectedPartitionProfileIndex = $selectedIndex $script:SelectedPartitionProfile = $candidateProfile Write-Log "Profil de partitionnement sélectionné : $($candidateProfile.Name)" 'INFO' if ($script:SelectedPartitionSizing) { foreach ($role in $script:SelectedPartitionSizing.Inputs.Keys) { Write-Log "Taille saisie pour $role : $($script:SelectedPartitionSizing.Inputs[$role]) Go ; espace restant : $($script:SelectedPartitionSizing.RemainderGB) Go" 'INFO' } } } }) $script:cmbDisk.Add_SelectedIndexChanged({ if ($script:RefreshingDiskList -or $script:ChangingPartitionProfileSelection) { return } $diskInfo = Get-SelectedDeployDiskInfo if ($null -eq $diskInfo) { $script:SelectedDiskIndex = $null; return } $profileSizeInputs = @($script:SelectedPartitionProfile.Partitions | Where-Object { $_.Size.Policy -eq 'UserInputGB' }) if ($profileSizeInputs.Count -gt 0) { $initialValues = if ($script:SelectedPartitionSizing -and $script:SelectedPartitionSizing.ProfileId -eq $script:SelectedPartitionProfile.Id) { $script:SelectedPartitionSizing.Inputs } else { @{} } if (-not (Set-PartitionProfileSizingSelection -Profile $script:SelectedPartitionProfile -DiskInfo $diskInfo -InitialValues $initialValues)) { $script:RefreshingDiskList = $true try { for ($i = 0; $i -lt $script:cmbDisk.Items.Count; $i++) { if ([string]$script:cmbDisk.Items[$i].Tag -eq [string]$script:SelectedDiskIndex) { $script:cmbDisk.SelectedIndex = $i; break } } } finally { $script:RefreshingDiskList = $false } return } } $script:SelectedDiskIndex = [string]$diskInfo.Index }) $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() }