Créer un ZIP d’un dossier avec progression en .NET (exemple WPF)
Créez un ZIP depuis un dossier en .NET et mettez à jour une ProgressBar WPF via ZipArchive et IProgress pour une UI fluide.
ZipFile.CreateFromDirectory est pratique, mais ne fournit pas de mises à jour de progression.
Dans les applications desktop (surtout en WPF), il est souvent important d’afficher une barre de progression
lorsqu’on zippe un gros dossier.
Dans cet exemple, le ZIP est créé avec ZipArchive et la progression est reportée via IProgress<T>.
Côté UI, un ViewModel WPF simple met à jour une ProgressBar et un texte de statut grâce à
INotifyPropertyChanged.
Objectif
- Créer un ZIP à partir d’un dossier.
- Afficher la progression (pourcentage + fichier courant).
- Garder l’UI fluide (async + callbacks de progression).
- Permettre de sélectionner le dossier source et le chemin du ZIP depuis l’UI.
Zip Helper (ZipArchive + Progress)
Le helper parcourt tous les fichiers du dossier, crée des entrées dans ZipArchive et copie les streams
des fichiers dans l’archive. Après chaque fichier ajouté, il reporte la progression via
IProgress<ZipProgressInfo>.
- Pourquoi ZipArchive ? Il donne un contrôle total sur la création des entrées et le moment où la progression est reportée.
- Pourquoi une progression par fichier ? C’est simple et généralement suffisant pour l’UX. (Une progression au byte est possible, mais plus complexe.)
- Chemins relatifs : Les entrées ZIP doivent utiliser des chemins relatifs au dossier sélectionné.
using System.IO.Compression;
public sealed record ZipProgressInfo(
int FilesProcessed,
int TotalFiles,
string CurrentFile)
{
public double Percent => TotalFiles == 0 ? 0 : (FilesProcessed * 100.0 / TotalFiles);
}
public static class ZipHelper
{
public static async Task ZipDirectoryAsync(
string sourceDirectory,
string zipFilePath,
IProgress<ZipProgressInfo>? progress,
CancellationToken ct)
{
if (!Directory.Exists(sourceDirectory))
throw new DirectoryNotFoundException(sourceDirectory);
var files = Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories).ToList();
var total = files.Count;
// S'assurer que le dossier de sortie existe
var outDir = Path.GetDirectoryName(zipFilePath);
if (!string.IsNullOrWhiteSpace(outDir))
Directory.CreateDirectory(outDir);
// Écraser si le fichier existe déjà
if (File.Exists(zipFilePath))
File.Delete(zipFilePath);
await using var zipStream = new FileStream(zipFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: false);
var processed = 0;
foreach (var filePath in files)
{
ct.ThrowIfCancellationRequested();
var entryName = Path.GetRelativePath(sourceDirectory, filePath)
.Replace('\\', '/'); // ZIP préfère '/'
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
await using (var entryStream = entry.Open())
await using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await fileStream.CopyToAsync(entryStream, ct);
}
processed++;
progress?.Report(new ZipProgressInfo(
FilesProcessed: processed,
TotalFiles: total,
CurrentFile: entryName));
}
}
}
ViewModel WPF (Pourquoi INotifyPropertyChanged ?)
En WPF, les contrôles peuvent se binder aux propriétés du ViewModel. Lorsque la valeur d’une propriété change,
l’UI se met à jour automatiquement si le ViewModel déclenche l’évènement PropertyChanged.
C’est précisément le rôle de INotifyPropertyChanged.
Ce ViewModel contient :
SourceFolderetZipPath(sélectionnés par l’utilisateur)ProgressValue(0-100) etStatusText(fichier courant)- Des commandes pour sélectionner les chemins et lancer/annuler le processus de ZIP
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Microsoft.Win32;
public sealed class ZipFolderViewModel : INotifyPropertyChanged
{
private string _sourceFolder = "";
private string _zipPath = "";
private double _progressValue;
private string _statusText = "Prêt.";
private bool _isBusy;
private CancellationTokenSource? _cts;
public string SourceFolder
{
get => _sourceFolder;
set { _sourceFolder = value; OnPropertyChanged(); StartZipCommand.RaiseCanExecuteChanged(); }
}
public string ZipPath
{
get => _zipPath;
set { _zipPath = value; OnPropertyChanged(); StartZipCommand.RaiseCanExecuteChanged(); }
}
public double ProgressValue
{
get => _progressValue;
set { _progressValue = value; OnPropertyChanged(); }
}
public string StatusText
{
get => _statusText;
set { _statusText = value; OnPropertyChanged(); }
}
public bool IsBusy
{
get => _isBusy;
set
{
_isBusy = value;
OnPropertyChanged();
SelectFolderCommand.RaiseCanExecuteChanged();
SelectZipPathCommand.RaiseCanExecuteChanged();
StartZipCommand.RaiseCanExecuteChanged();
CancelCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand SelectFolderCommand { get; }
public RelayCommand SelectZipPathCommand { get; }
public AsyncRelayCommand StartZipCommand { get; }
public RelayCommand CancelCommand { get; }
public ZipFolderViewModel()
{
SelectFolderCommand = new RelayCommand(SelectFolder, () => !IsBusy);
SelectZipPathCommand = new RelayCommand(SelectZipPath, () => !IsBusy);
StartZipCommand = new AsyncRelayCommand(StartZipAsync, CanStart);
CancelCommand = new RelayCommand(Cancel, () => IsBusy);
}
private bool CanStart()
=> !IsBusy
&& !string.IsNullOrWhiteSpace(SourceFolder)
&& !string.IsNullOrWhiteSpace(ZipPath);
private void SelectFolder()
{
var dlg = new Microsoft.Win32.OpenFolderDialog
{
Title = "Sélectionner un dossier",
Multiselect = false
};
bool? ok = dlg.ShowDialog();
if (ok == true)
{
SourceFolder = dlg.FolderName;
}
}
private void SelectZipPath()
{
var dlg = new SaveFileDialog
{
Title = "Enregistrer le ZIP",
Filter = "ZIP files (*.zip)|*.zip",
DefaultExt = "zip",
AddExtension = true
};
if (dlg.ShowDialog() == true)
ZipPath = dlg.FileName;
}
private async Task StartZipAsync()
{
IsBusy = true;
ProgressValue = 0;
StatusText = "Démarrage...";
_cts = new CancellationTokenSource();
try
{
var progress = new Progress<ZipProgressInfo>(p =>
{
ProgressValue = p.Percent;
StatusText = $"{p.FilesProcessed}/{p.TotalFiles} - {p.CurrentFile}";
});
await ZipHelper.ZipDirectoryAsync(SourceFolder, ZipPath, progress, _cts.Token);
ProgressValue = 100;
StatusText = "Terminé.";
}
catch (OperationCanceledException)
{
StatusText = "Annulé.";
}
catch (Exception ex)
{
StatusText = $"Erreur : {ex.Message}";
}
finally
{
_cts?.Dispose();
_cts = null;
IsBusy = false;
}
}
private void Cancel()
{
_cts?.Cancel();
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Remarque : Progress<T> renvoie les mises à jour vers le contexte UI capturé (s’il est créé sur le thread UI).
Mettre à jour des propriétés bindées est donc sûr.
Remarque : Cet exemple utilise Microsoft.Win32.OpenFolderDialog, disponible en WPF à partir de .NET 8+.
Si vous ciblez une version plus ancienne, WPF ne fournit pas de sélecteur de dossier intégré. Dans ce cas :
-
Utiliser un sélecteur WinForms (
System.Windows.Forms.FolderBrowserDialog) dans une app WPF, ou - Utiliser une bibliothèque dédiée (par ex. Windows API Code Pack / Common File Dialog).
XAML (Boutons + ProgressBar + binding)
Cette vue propose :
- Un bouton pour sélectionner le dossier source
- Un bouton pour choisir le chemin du ZIP
- Des boutons Démarrer et Annuler
- Une ProgressBar + un texte de statut
<Window x:Class="ZipDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Zipper un dossier" Height="260" Width="560">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,10,10"
VerticalAlignment="Center" Text="Dossier source :" />
<TextBox Grid.Row="0" Grid.Column="1" Margin="0,0,10,10"
Text="{Binding SourceFolder}" IsReadOnly="True" />
<Button Grid.Row="0" Grid.Column="2" Margin="0,0,0,10"
Content="Choisir..."
Command="{Binding SelectFolderCommand}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,10"
VerticalAlignment="Center" Text="Chemin du ZIP :" />
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,10,10"
Text="{Binding ZipPath}" IsReadOnly="True" />
<Button Grid.Row="1" Grid.Column="2" Margin="0,0,0,10"
Content="Enregistrer..."
Command="{Binding SelectZipPathCommand}" />
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
Orientation="Horizontal" Margin="0,0,0,10">
<Button Width="110" Margin="0,0,10,0"
Content="Démarrer"
Command="{Binding StartZipCommand}" />
<Button Width="110"
Content="Annuler"
Command="{Binding CancelCommand}" />
</StackPanel>
<ProgressBar Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
Height="16" Minimum="0" Maximum="100"
Value="{Binding ProgressValue}" />
<TextBlock Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" Margin="0,8,0,0"
Text="{Binding StatusText}" TextTrimming="CharacterEllipsis" />
</Grid>
</Window>
Connecter le ViewModel (DataContext)
Il existe deux façons courantes de relier la View (XAML) au ViewModel :
- Code-behind (le plus simple pour une démo) : définir
DataContextdans le constructeur deMainWindow. - XAML (si le ViewModel a un constructeur sans paramètre) : le déclarer dans
Window.DataContext.
Pour cet exemple, l’approche la plus simple est le code-behind :
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ZipFolderViewModel();
}
}
Variante XAML (si le ViewModel peut être créé sans paramètres) :
<Window ...
xmlns:local="clr-namespace:ZipDemo">
<Window.DataContext>
<local:ZipFolderViewModel />
</Window.DataContext>
<!-- ... -->
</Window>
Petits helpers de commandes (RelayCommand / AsyncRelayCommand)
Les commandes WPF évitent de mettre de la logique UI dans le code-behind. Voici une implémentation minimale :
using System.Windows.Input;
public sealed class RelayCommand(Action execute, Func<bool>? canExecute = null) : ICommand
{
private readonly Action _execute = execute;
private readonly Func<bool> _canExecute = canExecute ?? (() => true);
public bool CanExecute(object? parameter) => _canExecute();
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public sealed class AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null) : ICommand
{
private readonly Func<Task> _execute = execute;
private readonly Func<bool> _canExecute = canExecute ?? (() => true);
private bool _isRunning;
public bool CanExecute(object? parameter) => !_isRunning && _canExecute();
public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;
try
{
_isRunning = true;
RaiseCanExecuteChanged();
await _execute();
}
finally
{
_isRunning = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
Améliorations courantes
- Reporter la progression en bytes (plus précis si la taille des fichiers varie beaucoup).
- Ignorer les fichiers verrouillés et collecter des warnings (au lieu d’échouer tout le processus).
- Lancer plusieurs compressions en parallèle avec prudence (l’I/O disque devient vite le goulot d’étranglement).
- Créer le ViewModel via dependency injection (au lieu de faire
newdans le code-behind).
TL;DR
ZipArchiveoffre le contrôle nécessaire pour reporter la progression.IProgress<T>rend les mises à jour de progression simples et sûres côté UI en WPF.INotifyPropertyChangedmet à jour l’UI automatiquement lorsque les propriétés du ViewModel changent.- Un petit setup MVVM (commands + bindings) garde l’UI fluide et propre.