C#のWPFでファイルマネージャを作る

コンピュータ

古いエクスプローラーのような見た目のファイルマネージャをWPFの標準コントロールで実装する試みです。
TreeViewやListViewなどでデータバインディングは行っていますが、基本イベントドリブンをコードビハインドで記述するスタイル。

GitHubリポジトリ


実行イメージ


ソースコード

ファイル名:MyFileManager.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <ApplicationIcon>Assets\app.ico</ApplicationIcon>
  </PropertyGroup>

</Project>

ファイル名:App.xaml.cs


using System.Configuration;
using System.Data;
using System.Windows;

namespace MyFileManager;

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}

ファイル名:AssemblyInfo.cs


using System.Windows;

[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]

ファイル名:BoolToVisibilityConverter.cs


using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

/*
* Boolo値をVisibility列挙体に変換するコンバータ
*/

namespace MyFileManager;
public sealed class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
                          object parameter, CultureInfo culture)
    {
        bool flag = value is bool b && b;

        // ConverterParameter に "Invert" が来たら反転
        if (parameter as string == "Invert")
            flag = !flag;

        return flag ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType,
                              object parameter, CultureInfo culture)
        => throw new NotSupportedException();
}

ファイル名:DirectoryNode.cs


using System.Collections.ObjectModel;
using System.IO;

/*
* FoldeerTree用のディレクトリノード
*/

namespace MyFileManager;
public sealed class DirectoryNode
{
    public string Name { get; }
    public string FullPath { get; }

    public ObservableCollection<DirectoryNode> Children { get; }
        = new ObservableCollection<DirectoryNode>();

    public bool IsLoaded { get; private set; }
    public bool IsDummy { get; }

    private DirectoryNode(string path, bool isDummy)
    {
        FullPath = path;
        IsDummy = isDummy;

        Name = isDummy
            ? string.Empty
            : Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar));

        if (string.IsNullOrEmpty(Name))
            Name = path;
    }

    public DirectoryNode(string path)
        : this(path, isDummy: false)
    {
    }

    public static DirectoryNode CreateDummy(string parentPath)
        => new DirectoryNode(parentPath, isDummy: true);

    public void EnsureDummy()
    {
        if (Children.Count == 0)
        {
            Children.Add(CreateDummy(FullPath));
        }
    }

    public void LoadChildren()
    {
        if (IsLoaded) return;

        Children.Clear();
        IsLoaded = true;

        try
        {
            foreach (var dir in FileSystemUtil.GetDirs(FullPath))
            {
                var child = new DirectoryNode(dir);
                child.EnsureDummy();
                Children.Add(child);
            }
        }
        catch
        {
            // Explorer同様、例外は無視
        }
    }
}

ファイル名:FileItem.cs


/*
* FileItemクラスの定義
*/

namespace MyFileManager;
public class FileItem
{
    public string Name { get; set; } = "";
    public string FullPath { get; set; } = "";
    public string Type { get; set; } = "";   // File / Directory
    public long Size { get; set; }
}

ファイル名:FileSystemUtil.cs


using System.IO;

public static class FileSystemUtil
{
    // ここにファイルシステム関連のユーティリティメソッドを追加できます
    public static string[] GetDirs(string path)
    {
        return Directory
            .GetDirectories(path)
            .Select(d => new DirectoryInfo(d))
            .Where(di =>
                !di.Attributes.HasFlag(FileAttributes.Hidden) &&
                !di.Attributes.HasFlag(FileAttributes.System) &&
                !di.Name.StartsWith("."))
            .Select(di => di.FullName)
            .ToArray();
    }

    public static string[] GetFiles(string path)
    {
        return Directory
            .GetFiles(path)
            .Select(f => new FileInfo(f))
            .Where(fi =>
                !fi.Attributes.HasFlag(FileAttributes.Hidden) &&
                !fi.Attributes.HasFlag(FileAttributes.System) &&
                !fi.Name.StartsWith("."))
            .Select(fi => fi.FullName)
            .ToArray();
    }
    public static string[] GetDrives()
    {
        return DriveInfo
            .GetDrives()
            .Where(drive =>
                drive.IsReady &&
                (drive.DriveType.HasFlag(DriveType.Fixed)||drive.DriveType.HasFlag(DriveType.Network))
            )
            .Select(drive => drive.RootDirectory.FullName)
            .ToArray();
    }

}

ファイル名:MainWindow.AddressBar.cs


using System.IO;
using System.Windows;
using System.Windows.Input;

/*
* AddressBarの初期化とイベントハンドラ
*/

namespace MyFileManager;

public partial class MainWindow : Window
{
    public string CurrentDirectory { get; private set; } = "";

    private void UpButton_Click(object sender, RoutedEventArgs e)
    {
        var path = Path.GetDirectoryName(CurrentDirectory) ?? CurrentDirectory;
        SetAddressBarCurrentDirectory(path);
    }

    private void MoveButton_Click(object sender, RoutedEventArgs e)
    {
        Navigate(AddressTextBox.Text);
    }

    private void AddressTextBox_KeyDown(object sender, KeyEventArgs e)
    {
        // Enterキーで移動
        if (e.Key == Key.Enter)
        {
            Navigate(AddressTextBox.Text);
        }
    }

    private void Navigate(string path)
    {
        if (Directory.Exists(path))
        {
            SetAddressBarCurrentDirectory(path);
        }
        else
        {
            MessageBox.Show(
                "ディレクトリが存在しません。",
                "エラー",
                MessageBoxButton.OK,
                MessageBoxImage.Error);
        }
    }

    // アドレスバーのカレントディレクトリ設定
    private void SetAddressBarCurrentDirectory(string path)
    {
        if (CurrentDirectory == path) return;

        CurrentDirectory = path;
        AddressTextBox.Text = path;

        UpdateCurrentDirectory(path);
    }
}

ファイル名:MainWindow.FileListView.cs


using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Diagnostics;
using System.ComponentModel;
using System.Windows.Data;

/*
* FileListViewの初期化とイベントハンドラ
*/

namespace MyFileManager;

public partial class MainWindow : Window
{
    private string _currentDirectory = @"C:\";

    private async void SetFileListViewDirectory(string path)
    {
var sw = Stopwatch.StartNew();
        FileListView.Visibility = Visibility.Collapsed;
        StatusText.Text = "Loading...";

Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Render,
    new Action(() => { })
);
        _currentDirectory = path;
        var fileItems = new ObservableCollection<FileItem>();

        var dirTask = Task.Run(() => FileSystemUtil.GetDirs(path));

        var dirs = await dirTask;
        
        var fileTask = Task.Run(() => FileSystemUtil.GetFiles(path));
        foreach (var dir in dirs)
        {
            fileItems.Add(new FileItem
            {
                Name = Path.GetFileName(dir),
                FullPath = dir,
                Type = "Directory",
                Size = 0
            });
        }

        var files = await fileTask;
        foreach (var file in files)
        {
            var info = new FileInfo(file);
            fileItems.Add(new FileItem
            {
                Name = info.Name,
                FullPath = info.FullName,
                Type = "File",
                Size = info.Length
            });
        }
        FileListView.ItemsSource = fileItems;
        FileListView.UpdateLayout();

        FileListView.Visibility = Visibility.Visible;
sw.Stop();
        StatusText.Text = $"LoadingTime : {sw.ElapsedMilliseconds} ms";
    }

    // 選択変更(イベントドリブン)
    private void FileListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (FileListView.SelectedItem is FileItem item)
        {
            //System.Diagnostics.Debug.WriteLine($"Selected: {item.FullPath}");
        }
    }

    // ダブルクリック処理
    private void FileListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (FileListView.SelectedItem is FileItem item)
        {
            if (item.Type == "Directory")
            {
                UpdateCurrentDirectory(item.FullPath);
            }
            else
            {
                Process.Start(new ProcessStartInfo
                {
                    FileName = item.FullPath,
                    UseShellExecute = true
                });
            }
        }
    }

    /* 右クリックメニュー処理 */
    private FileItem? SelectedItem =>
        FileListView.SelectedItem as FileItem;
    private void FileListView_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        var element = e.OriginalSource as DependencyObject;

        while (element != null && element is not ListViewItem)
        {
            element = System.Windows.Media.VisualTreeHelper.GetParent(element);
        }

        if (element is ListViewItem item)
        {
            item.IsSelected = true;
            item.Focus();
        }
    }

    private void Menu_Open_Click(object sender, RoutedEventArgs e)
    {
        if (SelectedItem == null) return;

        if (SelectedItem.Type == "Directory")
        {
            UpdateCurrentDirectory(SelectedItem.FullPath);
        }
        else
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = SelectedItem.FullPath,
                UseShellExecute = true
            });
        }
    }

    private void Menu_OpenInExplorer_Click(object sender, RoutedEventArgs e)
    {
        if (SelectedItem == null) return;

        Process.Start("explorer.exe", $"/select,\"{SelectedItem.FullPath}\"");
    }

    private void Menu_Delete_Click(object sender, RoutedEventArgs e)
    {
        if (SelectedItem == null) return;

        if (MessageBox.Show(
            $"{SelectedItem.Name} を削除しますか?",
            "確認",
            MessageBoxButton.YesNo,
            MessageBoxImage.Warning) != MessageBoxResult.Yes)
            return;

        if (SelectedItem.Type == "Directory")
            Directory.Delete(SelectedItem.FullPath, true);
        else
            File.Delete(SelectedItem.FullPath);

        SetFileListViewDirectory(_currentDirectory);
    }

    /*
    * 列ヘッダクリックでソート
    */
    private ListSortDirection _nameSortDirection = ListSortDirection.Ascending;
    private ListSortDirection _sizeSortDirection = ListSortDirection.Ascending;
    private void NameHeader_Click(object sender, RoutedEventArgs e)
    {
        var view = CollectionViewSource.GetDefaultView(FileListView.ItemsSource);
        if (view == null) return;

        view.SortDescriptions.Clear();
        // 第1キー:種類
        view.SortDescriptions.Add(
            new SortDescription(nameof(FileItem.Type), _nameSortDirection));
        // 第2キー:名前
        view.SortDescriptions.Add(
            new SortDescription(nameof(FileItem.Name), _nameSortDirection));

        // 次回クリック用に反転
        _nameSortDirection =
            _nameSortDirection == ListSortDirection.Ascending
                ? ListSortDirection.Descending
                : ListSortDirection.Ascending;
    }
    private void SizeHeader_Click(object sender, RoutedEventArgs e)
    {
        var view = CollectionViewSource.GetDefaultView(FileListView.ItemsSource);
        if (view == null) return;

        view.SortDescriptions.Clear();
        // 第1キー:種類
        view.SortDescriptions.Add(
            new SortDescription(nameof(FileItem.Type), _sizeSortDirection));
        // 第2キー:サイズ
        view.SortDescriptions.Add(
            new SortDescription(nameof(FileItem.Size), _sizeSortDirection));

        // 次回クリック用に反転
        _sizeSortDirection =
            _sizeSortDirection == ListSortDirection.Ascending
                ? ListSortDirection.Descending
                : ListSortDirection.Ascending;
    }
}

ファイル名:MainWindow.FolderTree.cs


using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;

/*
* FolderTreeの初期化とイベントハンドラ
*/

namespace MyFileManager;

public partial class MainWindow : Window
{
    public ObservableCollection<DirectoryNode> RootNodes { get; }
        = new ObservableCollection<DirectoryNode>();
    
    private void LoadRoots()
    {
        RootNodes.Clear();

        foreach (var drive in FileSystemUtil.GetDrives())
        {
            var node = new DirectoryNode(drive);
            node.EnsureDummy();

            RootNodes.Add(node);
        }
    }

    private void FolderTreeItem_Expanded(object sender, RoutedEventArgs e)
    {
        if (sender is not TreeViewItem item) return;
        if (item.DataContext is not DirectoryNode node) return;

        node.LoadChildren();
    }

    private void FolderTree_SelectedItemChanged(
        object sender,
        RoutedPropertyChangedEventArgs<object> e)
    {
        if (e.NewValue is DirectoryNode node)
        {
            Title = node.FullPath;
            // ListView更新など
            UpdateCurrentDirectory(node.FullPath);
        }
    }

}

ファイル名:MainWindow.xaml.cs


using System.Windows;

namespace MyFileManager;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // FolderTreeの初期化
        DataContext = this;
        LoadRoots();

        // FileListViewの初期化
        SetFileListViewDirectory(_currentDirectory);

        // AddressBarの初期化
        SetAddressBarCurrentDirectory(_currentDirectory);

    }

    /* メニューイベントハンドラ */
    private void OnNewWindow(object sender, RoutedEventArgs e)
    {
        new MainWindow().Show();
    }

    private void OnExit(object sender, RoutedEventArgs e)
    {
        Close();
    }

    private void OnRefresh(object sender, RoutedEventArgs e)
    {
        StatusText.Text = "Refreshed";
    }

    private void OnAbout(object sender, RoutedEventArgs e)
    {
        MessageBox.Show(
            "Explorer Sample\nWPF Menu + StatusBar",
            "About",
            MessageBoxButton.OK,
            MessageBoxImage.Information);
    }

    /* ここまでメニューイベントハンドラ */

    /* カレントディレクトリの変更 */
    private void UpdateCurrentDirectory(string path)
    {
        // AddressBar更新
        if (CurrentDirectory != path)
        {
            SetAddressBarCurrentDirectory(path);
        }

        // FileListView更新
        if (_currentDirectory != path)
        {
            SetFileListViewDirectory(path);
        }
    }
}

ファイル名:App.xaml


<Application x:Class="MyFileManager.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MyFileManager"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

ファイル名:MainWindow.xaml


<Window x:Class="MyFileManager.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:MyFileManager"
        mc:Ignorable="d"
        FontSize="14"
        Title="MyFileManager" Height="450" Width="800">

    <Window.Resources>
        <local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
        <Style TargetType="MenuItem">
            <Setter Property="FontSize" Value="16"/>
        </Style>
    </Window.Resources>

    <DockPanel>

        <!-- メニュー ここから-->
        <Menu
            DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <MenuItem Header="_New Window" Click="OnNewWindow"/>
                <Separator/>
                <MenuItem Header="_Exit" Click="OnExit"/>
            </MenuItem>

            <MenuItem Header="_View">
                <MenuItem Header="Refresh" Click="OnRefresh"/>
            </MenuItem>

            <MenuItem Header="_Help">
                <MenuItem Header="_About" Click="OnAbout"/>
            </MenuItem>
        </Menu>
        <!-- メニュー ここまで-->

        <!-- ステータスバー -->
        <StatusBar
            DockPanel.Dock="Bottom">
            <StatusBarItem>
                <TextBlock x:Name="StatusText"
                           Text="Ready"/>
            </StatusBarItem>

            <Separator/>

            <StatusBarItem HorizontalAlignment="Right">
                <TextBlock x:Name="PathText"
                           Text="C:\"/>
            </StatusBarItem>
        </StatusBar>
        <!-- ステータスバー ここまで-->


        <DockPanel>
            <!-- アドレスバーと移動ボタン ここから -->
            <DockPanel
                DockPanel.Dock="Top"
                Margin="8">
                <!-- 上ボタン -->
                <Button Content="↑"
                        DockPanel.Dock="Left"
                        Width="20"
                        Click="UpButton_Click"/>
                <!-- 移動ボタン -->
                <Button Content="移動"
                        DockPanel.Dock="Right"
                        Width="40"
                        Click="MoveButton_Click"/>
                <!-- アドレスバー -->
                <TextBox x:Name="AddressTextBox"
                        Margin="4,0,4,0"
                        VerticalContentAlignment="Center"
                        KeyDown="AddressTextBox_KeyDown"/>
            </DockPanel>
            <!-- アドレスバーと移動ボタン ここまで -->

            <Grid>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="220" />
                    <ColumnDefinition Width="5" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                
                <!-- 左側フォルダツリー ここから -->
                <TreeView x:Name="FolderTree"
                        ItemsSource="{Binding RootNodes}"
                        SelectedItemChanged="FolderTree_SelectedItemChanged"
                        Grid.Column="0">

                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                            <TextBlock Text="{Binding Name}"
                                    Visibility="{Binding IsDummy,
                                                    Converter={StaticResource BoolToVisibilityConverter},
                                                    ConverterParameter=Invert}" />
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                    <TreeView.ItemContainerStyle>
                        <Style TargetType="TreeViewItem">
                            <EventSetter Event="Expanded"
                                        Handler="FolderTreeItem_Expanded"/>
                        </Style>
                    </TreeView.ItemContainerStyle>
                </TreeView>
                <!-- 左側フォルダツリー ここまで -->

                <!-- 左右の仕切り ここから-->
                <GridSplitter
                    Grid.Column="1"
                    HorizontalAlignment="Stretch" />  
                <!-- 左右の仕切り ここまで-->

                <!-- 右側ファイルリストビュー ここから -->
                <ListView
                    x:Name="FileListView"
                    Grid.Column="2"
                    SelectionChanged="FileListView_SelectionChanged"
                    MouseDoubleClick="FileListView_MouseDoubleClick">

                    <!-- コンテキストメニュー ここから -->
                    <ListView.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="開く"
                                    Click="Menu_Open_Click" />
                            <MenuItem Header="エクスプローラーで開く"
                                    Click="Menu_OpenInExplorer_Click" />
                            <Separator/>
                            <MenuItem Header="削除"
                                    Click="Menu_Delete_Click" />
                        </ContextMenu>
                    </ListView.ContextMenu>
                    <!-- コンテキストメニュー ここまで -->
                    
                    <ListView.ItemContainerStyle>
                        <Style TargetType="ListViewItem">
                            <Setter
                                Property="HorizontalContentAlignment"
                                Value="Stretch"/>
                        </Style>
                    </ListView.ItemContainerStyle>


                    <ListView.View>
                        <GridView>
                            <GridViewColumn Width="300">
                                <GridViewColumnHeader
                                    Content="名前"
                                    Click="NameHeader_Click"
                                    HorizontalContentAlignment="Left"/>
                                <GridViewColumn.DisplayMemberBinding>
                                    <Binding Path="Name"/>
                                </GridViewColumn.DisplayMemberBinding>
                            </GridViewColumn>
                            <GridViewColumn Width="100">
                                <GridViewColumnHeader
                                    Content="種類"
                                    Click="NameHeader_Click"
                                    HorizontalContentAlignment="Left"/>
                                <GridViewColumn.DisplayMemberBinding>
                                    <Binding Path="Type"/>
                                </GridViewColumn.DisplayMemberBinding>
                            </GridViewColumn>
                            <GridViewColumn Width="100">
                                <GridViewColumnHeader
                                    Content="サイズ"
                                    Click="SizeHeader_Click"
                                    HorizontalContentAlignment="Right"/>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock
                                            Text="{Binding Size}"
                                            HorizontalAlignment="Right"/>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>

                        </GridView>
                    </ListView.View>
                </ListView>
                <!-- 右側ファイルリストビュー ここまで -->

            </Grid>
        </DockPanel>

    </DockPanel>
</Window>

コメント