Ordner als ZIP erstellen mit Fortschritt in .NET (WPF-Beispiel)
Einen Ordner in .NET als ZIP erstellen und den Fortschritt per ZipArchive und IProgress sauber in einer WPF ProgressBar anzeigen.
ZipFile.CreateFromDirectory ist praktisch, liefert aber keine Fortschrittsinformationen.
In Desktop-Apps (insbesondere WPF) ist es oft wichtig, beim Zippen eines großen Ordners eine Progressbar anzuzeigen.
In diesem Beispiel wird das ZIP mit ZipArchive erstellt und der Fortschritt über IProgress<T> gemeldet.
Auf der UI-Seite aktualisiert ein simples WPF-ViewModel eine ProgressBar und einen Status-Text über
INotifyPropertyChanged.
Ziel
- Einen Ordner in eine ZIP-Datei packen.
- Fortschritt anzeigen (Prozent + aktuelle Datei).
- UI responsiv halten (async + Progress-Callbacks).
- Im UI Quellordner und Zielpfad für die ZIP auswählen können.
Zip Helper (ZipArchive + Progress)
Der Helper enumeriert alle Dateien im Ordner, erstellt für jede Datei einen Eintrag im ZipArchive und kopiert
die Dateistreams in das Archiv. Nach jeder hinzugefügten Datei wird der Fortschritt über
IProgress<ZipProgressInfo> gemeldet.
- Warum ZipArchive? Volle Kontrolle darüber, wie Einträge erzeugt werden und wann Fortschritt gemeldet wird.
- Warum Fortschritt pro Datei? Einfach und für UX meist ausreichend. (Byte-genauer Fortschritt ist möglich, aber komplexer.)
- Relative Pfade: ZIP-Einträge sollten relativ zum ausgewählten Ordner angelegt werden.
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;
// Ausgabeordner sicherstellen
var outDir = Path.GetDirectoryName(zipFilePath);
if (!string.IsNullOrWhiteSpace(outDir))
Directory.CreateDirectory(outDir);
// Überschreiben, falls vorhanden
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 bevorzugt '/'
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));
}
}
}
WPF ViewModel (Warum INotifyPropertyChanged?)
In WPF können Controls an ViewModel-Properties gebunden werden. Wenn sich eine Property ändert, aktualisiert sich die UI automatisch,
wenn das ViewModel das PropertyChanged-Event auslöst. Genau dafür ist INotifyPropertyChanged da.
Dieses ViewModel enthält:
SourceFolderundZipPath(vom Benutzer ausgewählt)ProgressValue(0-100) undStatusText(aktuelle Datei)- Commands zum Pfad auswählen und zum Starten/Abbrechen des ZIP-Vorgangs
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 = "Bereit.";
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 = "Ordner auswählen",
Multiselect = false
};
bool? ok = dlg.ShowDialog();
if (ok == true)
{
SourceFolder = dlg.FolderName;
}
}
private void SelectZipPath()
{
var dlg = new SaveFileDialog
{
Title = "ZIP-Datei speichern",
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 = "Startet...";
_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 = "Fertig.";
}
catch (OperationCanceledException)
{
StatusText = "Abgebrochen.";
}
catch (Exception ex)
{
StatusText = $"Fehler: {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));
}
Hinweis: Progress<T> postet Updates zurück in den erfassten UI-Context (wenn auf dem UI-Thread erstellt).
Dadurch ist das Aktualisieren gebundener Properties sicher.
Hinweis: Dieses Beispiel nutzt Microsoft.Win32.OpenFolderDialog, das für WPF ab .NET 8+ verfügbar ist.
Wenn ein älteres .NET-Target verwendet wird, gibt es in WPF keinen eingebauten Ordner-Dialog. In diesem Fall:
-
Einen WinForms-Ordnerdialog (
System.Windows.Forms.FolderBrowserDialog) in einer WPF-App verwenden, oder - Eine spezielle Folder-Picker-Bibliothek nutzen (z. B. Windows API Code Pack / Common File Dialog).
XAML (Buttons + ProgressBar + Binding)
Diese View bietet:
- Button zum Auswählen des Quellordners
- Button zum Auswählen des ZIP-Zielpfads
- Start- und Abbrechen-Buttons
- ProgressBar + Status-Text
<Window x:Class="ZipDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Ordner zippen" 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="Quellordner:" />
<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="Auswählen..."
Command="{Binding SelectFolderCommand}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,10"
VerticalAlignment="Center" Text="ZIP-Pfad:" />
<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="Speichern..."
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="Start"
Command="{Binding StartZipCommand}" />
<Button Width="110"
Content="Abbrechen"
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>
ViewModel anbinden (DataContext)
Es gibt zwei gängige Wege, View (XAML) und ViewModel zu verbinden:
- Code-behind (für Demos am einfachsten):
DataContextimMainWindow-Konstruktor setzen. - XAML (wenn das ViewModel parameterlos erstellt werden kann): innerhalb von
Window.DataContextdeklarieren.
Für dieses Beispiel ist Code-behind die einfachste Variante:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ZipFolderViewModel();
}
}
Alternativ in XAML (wenn das ViewModel ohne Parameter erstellt werden kann):
<Window ...
xmlns:local="clr-namespace:ZipDemo">
<Window.DataContext>
<local:ZipFolderViewModel />
</Window.DataContext>
<!-- ... -->
</Window>
Kleine Command-Helper (RelayCommand / AsyncRelayCommand)
WPF-Commands halten UI-Logik aus dem Code-behind heraus. Hier ist eine minimale Implementierung:
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);
}
Häufige Verbesserungen
- Fortschritt nach Bytes statt nach Dateien melden (genauer bei stark unterschiedlichen Dateigrößen).
- Gesperrte Dateien überspringen und Warnungen sammeln (statt den gesamten Vorgang zu stoppen).
- Paralleles Komprimieren nur mit Vorsicht (Disk-I/O wird sehr schnell zum Bottleneck).
- ViewModel per Dependency Injection erstellen statt im Code-behind zu
newen.
TL;DR
ZipArchivebietet die Kontrolle, die für Progress-Reporting nötig ist.IProgress<T>macht Progress-Updates in WPF einfach und UI-sicher.INotifyPropertyChangedaktualisiert die UI automatisch, wenn sich ViewModel-Properties ändern.- Ein kleines MVVM-Setup (Commands + Bindings) hält die UI responsiv und sauber.