WPF C#/VB
How to Analyse Storage on Your PC
C#
// AZUL CODING ---------------------------------------
// WPF C#/VB - How to Analyse Storage on Your PC
// https://youtu.be/M-V2GKO7fh4
// Looking for an example app? Get Quota Express:
// https://johnjds.co.uk/quota?ref=azul
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
// Plugin: https://www.nuget.org/packages/WindowsAPICodePackShell
using Microsoft.WindowsAPICodePack.Dialogs;
using Microsoft.WindowsAPICodePack.Shell;
namespace AzulStorageAnalysis
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly ObservableCollection<FileItem> FileDisplayList = [];
private CancellationTokenSource CancelTokenSource = new();
private Task? FileSizeTask;
private string CurrentRoot = "";
private readonly CommonOpenFileDialog FolderBrowserDialog = new()
{
IsFolderPicker = true,
Multiselect = false
};
public MainWindow()
{
InitializeComponent();
FolderItems.ItemsSource = FileDisplayList;
DriveCombo.ItemsSource = DriveInfo.GetDrives().Select(d =>
{
string displayName = d.DriveType switch
{
DriveType.Removable => "USB Drive " + d.Name,
DriveType.Network => "Network Drive " + d.Name,
DriveType.CDRom => "CD Drive " + d.Name,
_ => "Drive " + d.Name
};
return new ComboBoxItem()
{
Content = displayName,
Tag = d.Name
};
});
DriveCombo.SelectedIndex = 0;
GetFiles(KnownFolders.Downloads?.Path ?? "");
}
#region Folder Picker
private void ChooseBtn_Click(object sender, RoutedEventArgs e)
{
if (FolderBrowserDialog.ShowDialog() == CommonFileDialogResult.Ok)
GetFiles(FolderBrowserDialog.FileName ?? "");
}
private void FilePathTxt_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && !string.IsNullOrWhiteSpace(FilePathTxt.Text))
{
if (Path.Exists(FilePathTxt.Text))
{
GetFiles(FilePathTxt.Text);
}
else
{
MessageBox.Show("The file path you entered doesn't exist.");
FilePathTxt.Text = CurrentRoot;
}
}
}
#endregion
#region File Items
private void FileItem_Click(object sender, RoutedEventArgs e)
{
FileItem item = (FileItem)((Button)sender).Tag;
if (item.IsFolder)
{
GetFiles(item.FilePath);
}
else
{
try
{
_ = Process.Start(new ProcessStartInfo()
{
FileName = item.FilePath,
UseShellExecute = true
});
}
catch
{
MessageBox.Show("Can't open file.");
}
}
}
private async void GetFiles(string path)
{
try
{
string[] folders = Directory.GetDirectories(path);
string[] files = Directory.GetFiles(path);
if (FileSizeTask != null && !FileSizeTask.IsCompleted)
{
CancelTokenSource.Cancel();
await FileSizeTask;
}
CancelTokenSource = new();
FileDisplayList.Clear();
foreach (string item in folders)
FileDisplayList.Add(new FileItem()
{
FileName = Path.GetFileName(item),
FilePath = item,
IsFolder = true
});
foreach (string item in files)
FileDisplayList.Add(new FileItem()
{
FileName = Path.GetFileName(item),
FilePath = item,
IsFolder = false
});
FilePathTxt.Text = CurrentRoot = path;
FolderSizeLbl.Text = "Calculating...";
Scroller.ScrollToTop();
FileSizeTask = Task.Run(() => GetFileSizes(CancelTokenSource.Token));
await FileSizeTask;
if (!FileSizeTask.IsCanceled)
FolderSizeLbl.Text = FormatBytes(FileDisplayList.Sum(x => x.FileSize));
}
catch (Exception ex)
{
MessageBox.Show("An error occurred: " + ex.Message);
}
}
private void GetFileSizes(CancellationToken cancellationToken)
{
foreach (FileItem item in FileDisplayList)
{
if (cancellationToken.IsCancellationRequested)
return;
try
{
if (item.IsFolder)
item.FileSize = GetDirSize(new DirectoryInfo(item.FilePath), cancellationToken);
else
item.FileSize = new FileInfo(item.FilePath).Length;
}
catch
{
item.FileSize = 0L;
}
}
}
#endregion
#region Drive Analysis
private void DriveCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
string driveName = (string)((ComboBoxItem)DriveCombo.SelectedValue).Tag;
DriveInfo d = new(driveName);
DriveSpaceTakenLbl.Text = FormatBytes(d.TotalSize - d.TotalFreeSpace);
DriveRemainingSpaceLbl.Text = FormatBytes(d.TotalFreeSpace);
DriveTotalSpaceLbl.Text = FormatBytes(d.TotalSize);
}
catch
{
DriveSpaceTakenLbl.Text = DriveRemainingSpaceLbl.Text = DriveTotalSpaceLbl.Text = "—";
}
}
#endregion
#region Helper Functions
private static long GetDirSize(DirectoryInfo d, CancellationToken cancellationToken)
{
long size = 0L;
try
{
FileInfo[] fis = d.GetFiles();
foreach (FileInfo fi in fis)
{
if (cancellationToken.IsCancellationRequested)
return 0L;
size += fi.Length;
}
DirectoryInfo[] dis = d.GetDirectories();
foreach (DirectoryInfo di in dis)
{
if (cancellationToken.IsCancellationRequested)
return 0L;
size += GetDirSize(di, cancellationToken);
}
}
catch { }
if (cancellationToken.IsCancellationRequested)
return 0L;
else
return size;
}
public static string FormatBytes(long bytes)
{
double bytesConverted;
string result = "—";
try
{
switch (bytes)
{
case >= 1125899906842625:
result = "1000+ TB";
break;
case >= 1099511627776:
bytesConverted = bytes / (double)1099511627776;
result = Math.Round(bytesConverted, 2).ToString() + " TB";
break;
case >= 1073741824:
bytesConverted = bytes / (double)1073741824;
result = Math.Round(bytesConverted, 2).ToString() + " GB";
break;
case >= 1048576:
bytesConverted = bytes / (double)1048576;
result = Math.Round(bytesConverted, 2).ToString() + " MB";
break;
case >= 1024:
bytesConverted = bytes / (double)1024;
result = Math.Round(bytesConverted, 2).ToString() + " KB";
break;
case >= 1:
bytesConverted = bytes;
result = Math.Round(bytesConverted, 2).ToString() + " b";
break;
default:
return result;
}
return result;
}
catch
{
return result;
}
}
#endregion
}
#region Helper Classes
public class FileItem : INotifyPropertyChanged
{
public string FileName { get; set; } = "";
public string FilePath { get; set; } = "";
public bool IsFolder { get; set; } = false;
private long size = -1L;
public long FileSize
{
get { return size; }
set
{
if (size != value)
{
size = value;
OnPropertyChanged(nameof(FileSize));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class IconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ?
"https://img.icons8.com/fluency/48/folder-invoices--v1.png" :
"https://img.icons8.com/fluency/48/file.png";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return true;
}
}
public class FileSizeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (long)value == -1 ? "Calculating..." : MainWindow.FormatBytes((long)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return 0L;
}
}
#endregion
}
VB.NET
' AZUL CODING ---------------------------------------
' WPF C#/VB - How to Analyse Storage on Your PC
' https://youtu.be/M-V2GKO7fh4
' Looking for an example app? Get Quota Express:
' https://johnjds.co.uk/quota?ref=azul
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Globalization
Imports System.IO
Imports System.Threading
' Plugin: https://www.nuget.org/packages/WindowsAPICodePackShell
Imports Microsoft.WindowsAPICodePack.Dialogs
Imports Microsoft.WindowsAPICodePack.Shell
Class MainWindow
Inherits Window
Private ReadOnly FileDisplayList As New ObservableCollection(Of FileItem)
Private CancelTokenSource As New CancellationTokenSource()
Private FileSizeTask As Task
Private CurrentRoot As String = ""
Private ReadOnly FolderBrowserDialog As New CommonOpenFileDialog() With {
.IsFolderPicker = True,
.Multiselect = False
}
Public Sub New()
InitializeComponent()
FolderItems.ItemsSource = FileDisplayList
DriveCombo.ItemsSource = DriveInfo.GetDrives().Select(
Function(d)
Dim displayName As String = ""
Select Case d.DriveType
Case DriveType.Removable
displayName = "USB Drive " & d.Name
Case DriveType.Network
displayName = "Network Drive " & d.Name
Case DriveType.CDRom
displayName = "CD Drive " & d.Name
Case Else
displayName = "Drive " & d.Name
End Select
Return New ComboBoxItem() With {
.Content = displayName,
.Tag = d.Name
}
End Function
)
DriveCombo.SelectedIndex = 0
GetFiles(KnownFolders.Downloads.Path)
End Sub
#Region "Folder Picker"
Private Sub ChooseBtn_Click(sender As Object, e As RoutedEventArgs)
If FolderBrowserDialog.ShowDialog() = CommonFileDialogResult.Ok Then
GetFiles(FolderBrowserDialog.FileName)
End If
End Sub
Private Sub FilePathTxt_PreviewKeyDown(sender As Object, e As KeyEventArgs)
If e.Key = Key.Enter AndAlso Not String.IsNullOrWhiteSpace(FilePathTxt.Text) Then
If Path.Exists(FilePathTxt.Text) Then
GetFiles(FilePathTxt.Text)
Else
MessageBox.Show("The file path you entered doesn't exist.")
FilePathTxt.Text = CurrentRoot
End If
End If
End Sub
#End Region
#Region "File Items"
Private Sub FileItem_Click(sender As Object, e As RoutedEventArgs)
Dim item As FileItem = DirectCast(DirectCast(sender, Button).Tag, FileItem)
If item.IsFolder Then
GetFiles(item.FilePath)
Else
Try
Process.Start(New ProcessStartInfo() With {
.FileName = item.FilePath,
.UseShellExecute = True
})
Catch
MessageBox.Show("Can't open file.")
End Try
End If
End Sub
Private Async Sub GetFiles(path As String)
Try
Dim folders As String() = Directory.GetDirectories(path)
Dim files As String() = Directory.GetFiles(path)
If FileSizeTask IsNot Nothing AndAlso Not FileSizeTask.IsCompleted Then
CancelTokenSource.Cancel()
Await FileSizeTask
End If
CancelTokenSource = New CancellationTokenSource()
FileDisplayList.Clear()
For Each item In folders
FileDisplayList.Add(New FileItem() With {
.FileName = IO.Path.GetFileName(item),
.FilePath = item,
.IsFolder = True
})
Next
For Each item In files
FileDisplayList.Add(New FileItem() With {
.FileName = IO.Path.GetFileName(item),
.FilePath = item,
.IsFolder = False
})
Next
CurrentRoot = path
FilePathTxt.Text = CurrentRoot
FolderSizeLbl.Text = "Calculating..."
Scroller.ScrollToTop()
FileSizeTask = Task.Run(Sub() GetFileSizes(CancelTokenSource.Token))
Await FileSizeTask
If Not FileSizeTask.IsCanceled Then
FolderSizeLbl.Text = FormatBytes(FileDisplayList.Sum(Function(x) x.FileSize))
End If
Catch ex As Exception
MessageBox.Show("An error occurred: " & ex.Message)
End Try
End Sub
Private Sub GetFileSizes(cancellationToken As CancellationToken)
For Each item In FileDisplayList
If cancellationToken.IsCancellationRequested Then
Return
End If
Try
If item.IsFolder Then
item.FileSize = GetDirSize(New DirectoryInfo(item.FilePath), cancellationToken)
Else
item.FileSize = New FileInfo(item.FilePath).Length
End If
Catch
item.FileSize = 0L
End Try
Next
End Sub
#End Region
#Region "Drive Analysis"
Private Sub DriveCombo_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
Try
Dim driveName As String = DriveCombo.SelectedValue.Tag
Dim d As New DriveInfo(driveName)
DriveSpaceTakenLbl.Text = FormatBytes(d.TotalSize - d.TotalFreeSpace)
DriveRemainingSpaceLbl.Text = FormatBytes(d.TotalFreeSpace)
DriveTotalSpaceLbl.Text = FormatBytes(d.TotalSize)
Catch
DriveSpaceTakenLbl.Text = "—"
DriveRemainingSpaceLbl.Text = "—"
DriveTotalSpaceLbl.Text = "—"
End Try
End Sub
#End Region
#Region "Helper Functions"
Private Shared Function GetDirSize(d As DirectoryInfo, cancellationToken As CancellationToken) As Long
Dim size As Long = 0L
Try
Dim fis As FileInfo() = d.GetFiles()
For Each fi In fis
If cancellationToken.IsCancellationRequested Then
Return 0L
End If
size += fi.Length
Next
Dim dis As DirectoryInfo() = d.GetDirectories()
For Each di In dis
If cancellationToken.IsCancellationRequested Then
Return 0L
End If
size += GetDirSize(di, cancellationToken)
Next
Catch
End Try
If cancellationToken.IsCancellationRequested Then
Return 0L
Else
Return size
End If
End Function
Public Shared Function FormatBytes(bytes As Long) As String
Dim bytesConverted As Double
Dim result As String = "—"
Try
Select Case bytes
Case >= 1125899906842625L
result = "1000+ TB"
Case >= 1099511627776L
bytesConverted = bytes / 1099511627776.0
result = Math.Round(bytesConverted, 2).ToString() & " TB"
Case >= 1073741824L
bytesConverted = bytes / 1073741824.0
result = Math.Round(bytesConverted, 2).ToString() & " GB"
Case >= 1048576L
bytesConverted = bytes / 1048576.0
result = Math.Round(bytesConverted, 2).ToString() & " MB"
Case >= 1024L
bytesConverted = bytes / 1024.0
result = Math.Round(bytesConverted, 2).ToString() & " KB"
Case >= 1L
bytesConverted = bytes
result = Math.Round(bytesConverted, 2).ToString() & " b"
Case Else
Return result
End Select
Return result
Catch
Return result
End Try
End Function
#End Region
End Class
#Region "Helper Classes"
Public Class FileItem
Implements INotifyPropertyChanged
Public Property FileName As String = ""
Public Property FilePath As String = ""
Public Property IsFolder As Boolean = False
Private _size As Long = -1L
Public Property FileSize As Long
Get
Return _size
End Get
Set(value As Long)
If _size <> value Then
_size = value
OnPropertyChanged(NameOf(FileSize))
End If
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Overridable Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
Public Class IconConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
Return If(value,
"https://img.icons8.com/fluency/48/folder-invoices--v1.png",
"https://img.icons8.com/fluency/48/file.png")
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Return True
End Function
End Class
Public Class FileSizeConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
Return If(value = -1, "Calculating...", MainWindow.FormatBytes(value))
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Return 0L
End Function
End Class
#End Region
XAML
<!-- AZUL CODING --------------------------------------- -->
<!-- WPF C#/VB - How to Analyse Storage on Your PC -->
<!-- https://youtu.be/M-V2GKO7fh4 -->
<!-- change 'AzulStorageAnalysis' to the name of the project you've set up -->
<Window x:Class="AzulStorageAnalysis.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AzulStorageAnalysis"
mc:Ignorable="d"
Title="Azul Coding - Storage Analysis" Width="600" MinWidth="600" ResizeMode="CanResize" Height="425" MinHeight="425">
<Window.Resources>
<local:FileSizeConverter x:Key="FileSizeConverter"/>
<local:IconConverter x:Key="IconConverter"/>
</Window.Resources>
<DockPanel Background="White">
<Label x:Name="TitleLbl" DockPanel.Dock="Top" Content="Storage analysis" Padding="5,0,5,5" Margin="20,20,20,15" FontWeight="SemiBold" FontSize="16" BorderBrush="DodgerBlue" BorderThickness="0,0,0,2"/>
<Grid Margin="20,0,20,20" DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="3*" MaxWidth="250"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<DockPanel DockPanel.Dock="Top">
<Button Name="ChooseBtn" Click="ChooseBtn_Click" DockPanel.Dock="Right" Padding="5,0" Margin="5,0,0,0" Background="#f0f0f0" ToolTip="Choose folder">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Height="24" Width="24" Source="https://img.icons8.com/fluency/48/opened-folder.png"/>
</StackPanel>
</Button>
<TextBox x:Name="FilePathTxt" PreviewKeyDown="FilePathTxt_PreviewKeyDown" TextWrapping="Wrap" FontSize="14" Padding="5" VerticalAlignment="Center"/>
</DockPanel>
<ScrollViewer x:Name="Scroller" Margin="0,10,0,0" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="FolderItems">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Click="FileItem_Click" Tag="{Binding}" ToolTip="{Binding FilePath}" DockPanel.Dock="Right" Padding="5,3" Background="Transparent" BorderThickness="0" HorizontalContentAlignment="Stretch">
<DockPanel VerticalAlignment="Center">
<Image Height="24" Width="24" Source="{Binding Path=IsFolder, Converter={StaticResource IconConverter}}"/>
<TextBlock Text="{Binding Path=FileSize, Converter={StaticResource FileSizeConverter}}" DockPanel.Dock="Right" Margin="0,0,0,2" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding FileName}" Margin="10,0,10,2" FontSize="14" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
</DockPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<StackPanel Grid.Column="1" Margin="15,0,0,0" Background="AliceBlue">
<TextBlock Margin="20,15,20,0" FontSize="14" Text="Total folder size"/>
<TextBlock x:Name="FolderSizeLbl" Margin="20,0,20,15" FontSize="16" Text="0 MB" FontWeight="SemiBold"/>
<Rectangle Height="2" Fill="DodgerBlue" Margin="15,0"/>
<ComboBox x:Name="DriveCombo" SelectionChanged="DriveCombo_SelectionChanged" Margin="15,20,15,0" FontSize="14"/>
<TextBlock Margin="20,15,20,0" FontSize="14" Text="Space taken"/>
<TextBlock x:Name="DriveSpaceTakenLbl" Margin="20,0,20,0" FontSize="16" Text="0 MB" FontWeight="SemiBold"/>
<TextBlock Margin="20,15,20,0" FontSize="14" Text="Remaining space"/>
<TextBlock x:Name="DriveRemainingSpaceLbl" Margin="20,0,20,0" FontSize="16" Text="0 MB" FontWeight="SemiBold"/>
<TextBlock Margin="20,15,20,0" FontSize="14" Text="Total space"/>
<TextBlock x:Name="DriveTotalSpaceLbl" Margin="20,0,20,15" FontSize="16" Text="0 MB" FontWeight="SemiBold"/>
</StackPanel>
</Grid>
</DockPanel>
</Window>