WPFでExplorerライクなUIを作成する。

コンピュータ

ファイルマネージャの作成のため、Explorerを真似たUIの操作をWPFで再現してみたいと思います。

F2で名前の変更

ファイル名:Converter\BoolToVisibilityConverter.cs

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

namespace ExplorerModoki;

public class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value is bool b && b ? Visibility.Visible : Visibility.Collapsed;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => value is Visibility v && v == Visibility.Visible;
}





ファイル名:Converter\IInverseBoolToVisibilityConverter.cs

using System.Globalization;

namespace ExplorerModoki;
public interface IInverseBoolToVisibilityConverter
{
    object Convert(object value, Type targetType, object parameter, CultureInfo culture);
    object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

ファイル名:Converter\InverseBoolToVisibilityConverter.cs

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

namespace ExplorerModoki;

public class InverseBoolToVisibilityConverter : IValueConverter, IInverseBoolToVisibilityConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value is bool b && b ? Visibility.Collapsed : Visibility.Visible;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => value is Visibility v && v != Visibility.Visible;
}

ファイル名:ExplorerModoki.csproj

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:FileItem.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExplorerModoki;

public class FileItem : INotifyPropertyChanged
{
    string _name = "";
    string _originalName = "";
    bool _isEditing;

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (_isEditing == value) return;

            _isEditing = value;

            // 編集開始時に元の名前を保存
            if (_isEditing)
                _originalName = _name;

            OnPropertyChanged();
        }
    }
    public string OriginalName => _originalName;

    public bool IsFolder { get; set; }

    // 並び替え用
    public long Size { get; set; }
    public DateTime Modified { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml

<Window x:Class="ExplorerModoki.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:ExplorerModoki"
        mc:Ignorable="d"
        Title="ExplorerModoki" Height="450" Width="800">
    <Window.Resources>
        <local:BoolToVisibilityConverter x:Key="BoolToVis"/>
        <local:InverseBoolToVisibilityConverter x:Key="InvBoolToVis"/>
    </Window.Resources>
    <Grid>
        <ListView ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                KeyDown="ListView_KeyDown">

            <ListView.View>
                <GridView>

                    <!-- 名前列(編集対応) -->
                    <GridViewColumn Header="Name" Width="200">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <!-- 表示 -->
                                    <TextBlock
                                        Text="{Binding Name}"
                                        Visibility="{Binding IsEditing,
                                                    Converter={StaticResource InvBoolToVis}}"/>

                                    <!-- 編集 -->
                                    <TextBox
                                        Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
                                        Visibility="{Binding IsEditing,
                                                    Converter={StaticResource BoolToVis}}"
                                        IsVisibleChanged="RenameTextBox_IsVisibleChanged"
                                        LostFocus="RenameTextBox_LostFocus"
                                        KeyDown="RenameTextBox_KeyDown"/>
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <!-- サイズ列(表示のみ) -->
                    <GridViewColumn Header="Size" Width="80"
                                    DisplayMemberBinding="{Binding Size}" />

                    <!-- 更新日時列 -->
                    <GridViewColumn Header="Modified" Width="140"
                                    DisplayMemberBinding="{Binding Modified}" />

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

namespace ExplorerModoki;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    // ファイルの一覧、ListViewのItemSource
    public ObservableCollection<FileItem> Items {get; set;} = [];
    // 選択中のItem、バインド元
    FileItem? _selectedItem;
    public FileItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }
    }
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;

        this.Loaded += MainWindow_Loaded;
    }

    // ファイルの一覧取得(検証用)
    void MainWindow_Loaded(object? sender, RoutedEventArgs e)
    {
        Items.Clear();

        string path = @"C:\Users\karet\Pictures";

        var dir = new DirectoryInfo(path);

        // フォルダ
        foreach (var d in dir.GetDirectories())
        {
            Items.Add(new FileItem
            {
                Name = d.Name,
                IsFolder = true,
                Size = 0,
                Modified = d.LastWriteTime
            });
        }

        // ファイル
        foreach (var f in dir.GetFiles())
        {
            Items.Add(new FileItem
            {
                Name = f.Name,
                IsFolder = false,
                Size = f.Length,
                Modified = f.LastWriteTime
            });
        }

    }

    // F2キーで編集モード
    void ListView_KeyDown(object?senndr, KeyEventArgs e)
    {
        if (e.Key == Key.F2 && SelectedItem != null)
        {
            foreach (var item in Items)
                item.IsEditing = false;

            SelectedItem.IsEditing = true;
            e.Handled = true;
        }        
    }
    
    // TextBox フォーカス
    void RenameTextBox_IsVisibleChanged(object? sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is not TextBox tb) return;
        if (!tb.IsVisible) return;

        // フォーカス競合に勝つため Input 優先で遅延
        tb.Dispatcher.BeginInvoke(new Action(() =>
        {
            tb.Focus();
            Keyboard.Focus(tb);
            tb.SelectAll();
        }), DispatcherPriority.Input);  
    }

    // Enter / Esc で編集終了
    void RenameTextBox_KeyDown(object? sender, KeyEventArgs e)
    {
        if (sender is not TextBox tb) return;
        if (tb.DataContext is not FileItem item) return;

        if (e.Key == Key.Enter)
        {
            // 編集確定
            CommitRename(item);
            e.Handled = true;
        }
        else if (e.Key == Key.Escape)
        {
            // キャンセル
            item.Name = item.OriginalName;
            item.IsEditing = false;
            e.Handled = true;
        }
    }
    // フォーカスアウト
    void RenameTextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        if (sender is TextBox tb &&
            tb.DataContext is FileItem item &&
            item.IsEditing)
        {
            CommitRename(item);
        }
    }
    // 名前変更コミット
    void CommitRename(FileItem item)
    {
        item.IsEditing = false;

        if (item.Name != item.OriginalName)
        {
            Debug.WriteLine($"Rename: {item.OriginalName} → {item.Name}");
        }
    }
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

キーボードの「F2」を押すと編集モードになり、「エンターキー」で確定

コンテキストメニュー

ファイル名:ExplorerModoki2.csproj

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:FileItem.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExplorerModoki2;

public class FileItem : INotifyPropertyChanged
{
    string _name = "";
    string _originalName = "";
    bool _isEditing;

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (_isEditing == value) return;

            _isEditing = value;

            // 編集開始時に元の名前を保存
            if (_isEditing)
                _originalName = _name;

            OnPropertyChanged();
        }
    }
    public string OriginalName => _originalName;

    public bool IsFolder { get; set; }

    // 並び替え用
    public long Size { get; set; }
    public DateTime Modified { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml

<Window x:Class="ExplorerModoki2.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:ExplorerModoki2"
        mc:Ignorable="d"
        Title="コンテキストメニュー" Height="450" Width="800">
    <Window.Resources>

        <!-- FileItem 用メニュー -->
        <ContextMenu x:Key="FileItemMenu">
            <MenuItem Header="名前の変更" Click="RenameMenu_Click"/>
            <Separator/>
            <MenuItem Header="削除" Click="DeleteMenu_Click"/>
        </ContextMenu>

        <!-- 空白用メニュー -->
        <ContextMenu x:Key="BackgroundMenu">
            <MenuItem Header="更新" Click="RefreshMenu_Click"/>
            <MenuItem Header="並び替え">
                <MenuItem Header="名前" Click="SortByName_Click"/>
                <MenuItem Header="更新日時" Click="SortByModified_Click"/>
            </MenuItem>
        </ContextMenu>

    </Window.Resources>

    <Grid>
        <ListView x:Name="ListView"
                  ItemsSource="{Binding Items}"
                  SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                  ContextMenuOpening="ListView_ContextMenuOpening">
            <ListView.View>
                <GridView>

                    <!-- 名前列(編集対応) -->
                    <GridViewColumn Header="Name" Width="200">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock
                                        Text="{Binding Name}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <!-- サイズ列(表示のみ) -->
                    <GridViewColumn Header="Size" Width="80"
                                    DisplayMemberBinding="{Binding Size}" />

                    <!-- 更新日時列 -->
                    <GridViewColumn Header="Modified" Width="140"
                                    DisplayMemberBinding="{Binding Modified}" />

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;

namespace ExplorerModoki2;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    // ファイルの一覧、ListViewのItemSource
    public ObservableCollection<FileItem> Items {get; set;} = [];
    // 選択中のItem、バインド元
    FileItem? _selectedItem;
    public FileItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }
    }
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;

        this.Loaded += MainWindow_Loaded;
    }

    // ファイルの一覧取得(検証用)
    void MainWindow_Loaded(object? sender, RoutedEventArgs e)
    {
        Items.Clear();

        string path = @"C:\Users\karet\Pictures";

        var dir = new DirectoryInfo(path);

        // フォルダ
        foreach (var d in dir.GetDirectories())
        {
            Items.Add(new FileItem
            {
                Name = d.Name,
                IsFolder = true,
                Size = 0,
                Modified = d.LastWriteTime
            });
        }

        // ファイル
        foreach (var f in dir.GetFiles())
        {
            Items.Add(new FileItem
            {
                Name = f.Name,
                IsFolder = false,
                Size = f.Length,
                Modified = f.LastWriteTime
            });
        }

    }

    void ListView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
    {
        if (sender is not ListView lv) return;

        // 右クリック位置から ListViewItem を探す
        var element = e.OriginalSource as DependencyObject;
        var lvi = ItemsControl.ContainerFromElement(lv, element!) as ListViewItem;

        if (lvi != null)
        {
            // FileItem 上
            lv.SelectedItem = lvi.DataContext; // Explorer挙動:右クリックで選択移動
            lv.ContextMenu = (ContextMenu)FindResource("FileItemMenu");
        }
        else
        {
            // 空白部分
            lv.ContextMenu = (ContextMenu)FindResource("BackgroundMenu");
        }
    }
    // 名前の変更
    void RenameMenu_Click(object sender, RoutedEventArgs e)
    {
        Debug.Print($"変更:");
    }
    // 削除
    void DeleteMenu_Click(object sender, RoutedEventArgs e)
    {
        Debug.Print($"削除:");
    }    

    // 更新
    void RefreshMenu_Click(object sender, RoutedEventArgs e)
    {
        Debug.Print($"更新(ダミー)");
    }
    // 名前でソート
    void SortByName_Click(object sender, RoutedEventArgs e)
    {
        Debug.Print($"名前でソート");
    }
    // 更新日でソート
    void SortByModified_Click(object sender, RoutedEventArgs e)
    {
        Debug.Print($"更新日でソート");
    }
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイルItem上で右クリック→コンテキストメニュー
ファイルItem外で右クリック→コンテキストメニュー

領域によって異なるコンテキストメニューが表示される。

ヘッダークリックでソート

ファイル名:ExplorerModoki3.csproj

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:FileItem.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExplorerModoki3;

public class FileItem : INotifyPropertyChanged
{
    string _name = "";
    string _originalName = "";
    bool _isEditing;

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (_isEditing == value) return;

            _isEditing = value;

            // 編集開始時に元の名前を保存
            if (_isEditing)
                _originalName = _name;

            OnPropertyChanged();
        }
    }
    public string OriginalName => _originalName;

    public bool IsFolder { get; set; }

    // 並び替え用
    public long Size { get; set; }
    public DateTime Modified { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml

<Window x:Class="ExplorerModoki3.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:ExplorerModoki3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ListView x:Name="ListView"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                GridViewColumnHeader.Click="GridViewColumnHeader_Click">

            <ListView.View>
                <GridView>

                    <GridViewColumn Width="200">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Name" Tag="Name"/>
                        </GridViewColumn.Header>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock
                                        Text="{Binding Name}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Size}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Size" Tag="Size"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                    <GridViewColumn Width="140" DisplayMemberBinding="{Binding Modified}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Modified" Tag="Modified"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace ExplorerModoki3;

public partial class MainWindow  : Window, INotifyPropertyChanged
{

    // ソートの状態
    string? _currentSortProperty;
    ListSortDirection _currentSortDirection = ListSortDirection.Ascending;

    // ファイルの一覧、ListViewのItemSource
    public ObservableCollection<FileItem> Items {get; set;} = [];
    // 選択中のItem、バインド元
    FileItem? _selectedItem;
    public FileItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }
    }
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;

        this.Loaded += MainWindow_Loaded;
    }

    // ファイルの一覧取得(検証用)
    void MainWindow_Loaded(object? sender, RoutedEventArgs e)
    {
        Items.Clear();

        string path = @"C:\Users\karet\Pictures";

        var dir = new DirectoryInfo(path);

        // フォルダ
        foreach (var d in dir.GetDirectories())
        {
            Items.Add(new FileItem
            {
                Name = d.Name,
                IsFolder = true,
                Size = 0,
                Modified = d.LastWriteTime
            });
        }

        // ファイル
        foreach (var f in dir.GetFiles())
        {
            Items.Add(new FileItem
            {
                Name = f.Name,
                IsFolder = false,
                Size = f.Length,
                Modified = f.LastWriteTime
            });
        }

    }

    // ヘッダークリック
    void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
    {
        if (e.OriginalSource is not GridViewColumnHeader header)
            return;

        if (header.Tag is not string propertyName)
            return;

        // 同じ列 → 昇降順トグル
        if (_currentSortProperty == propertyName)
        {
            _currentSortDirection =
                _currentSortDirection == ListSortDirection.Ascending
                    ? ListSortDirection.Descending
                    : ListSortDirection.Ascending;
        }
        else
        {
            _currentSortProperty = propertyName;
            _currentSortDirection = ListSortDirection.Ascending;
        }

        ApplySort();
    }
    // ソート本体
    void ApplySort()
    {
        var view = CollectionViewSource.GetDefaultView(Items);
        view.SortDescriptions.Clear();

        // ★ Explorer 風:フォルダ優先
        view.SortDescriptions.Add(
            new SortDescription(nameof(FileItem.IsFolder),
                                ListSortDirection.Descending));

        // ★ 選択列でソート
        view.SortDescriptions.Add(
            new SortDescription(_currentSortProperty!,
                                _currentSortDirection));

        view.Refresh();
    }
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ヘッダーの「Size」をクリック

「Size」でソートされる。

Deleteキーで削除

ファイル名:ExplorerModoki4.csproj

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:FileItem.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExplorerModoki4;

public class FileItem : INotifyPropertyChanged
{
    string _name = "";
    string _originalName = "";
    bool _isEditing;

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (_isEditing == value) return;

            _isEditing = value;

            // 編集開始時に元の名前を保存
            if (_isEditing)
                _originalName = _name;

            OnPropertyChanged();
        }
    }
    public string OriginalName => _originalName;

    public bool IsFolder { get; set; }

    // 並び替え用
    public long Size { get; set; }
    public DateTime Modified { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml

<Window x:Class="ExplorerModoki4.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:ExplorerModoki4"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ListView x:Name="ListView"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                KeyDown="ListView_KeyDown">

            <ListView.View>
                <GridView>

                    <GridViewColumn Width="200">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Name" Tag="Name"/>
                        </GridViewColumn.Header>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock
                                        Text="{Binding Name}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Size}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Size" Tag="Size"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                    <GridViewColumn Width="140" DisplayMemberBinding="{Binding Modified}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Modified" Tag="Modified"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;

namespace ExplorerModoki4;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    // ファイルの一覧、ListViewのItemSource
    public ObservableCollection<FileItem> Items {get; set;} = [];
    // 選択中のItem、バインド元
    FileItem? _selectedItem;
    public FileItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }
    }
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;

        this.Loaded += MainWindow_Loaded;
    }

    // ファイルの一覧取得(検証用)
    void MainWindow_Loaded(object? sender, RoutedEventArgs e)
    {
        Items.Clear();

        string path = @"C:\Users\karet\Pictures";

        var dir = new DirectoryInfo(path);

        // フォルダ
        foreach (var d in dir.GetDirectories())
        {
            Items.Add(new FileItem
            {
                Name = d.Name,
                IsFolder = true,
                Size = 0,
                Modified = d.LastWriteTime
            });
        }

        // ファイル
        foreach (var f in dir.GetFiles())
        {
            Items.Add(new FileItem
            {
                Name = f.Name,
                IsFolder = false,
                Size = f.Length,
                Modified = f.LastWriteTime
            });
        }

    }

    // Deleteキーで削除
    void ListView_KeyDown(object? sender, KeyEventArgs e)
    {
        if (e.Key != Key.Delete)
            return;

        // Rename 中は削除しない(Explorer挙動)
        if (Items.Any(x => x.IsEditing))
            return;

        if (SelectedItem == null)
            return;

        // ダミー削除(UIのみ)
        Items.Remove(SelectedItem);

        e.Handled = true;
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

「desktop.ini」を選択した状態で、キーボードの「Delete」キーを押す。

「deskitop.ini」が削除される。

キーボード入力ジャンプ

ファイル名:ExplorerModoki4.csproj

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:FileItem.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ExplorerModoki4;

public class FileItem : INotifyPropertyChanged
{
    string _name = "";
    string _originalName = "";
    bool _isEditing;

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (_isEditing == value) return;

            _isEditing = value;

            // 編集開始時に元の名前を保存
            if (_isEditing)
                _originalName = _name;

            OnPropertyChanged();
        }
    }
    public string OriginalName => _originalName;

    public bool IsFolder { get; set; }

    // 並び替え用
    public long Size { get; set; }
    public DateTime Modified { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml

<Window x:Class="ExplorerModoki4.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:ExplorerModoki4"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ListView x:Name="ListView"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                PreviewTextInput="ListView_PreviewTextInput">

            <ListView.View>
                <GridView>

                    <GridViewColumn Width="200">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Name" Tag="Name"/>
                        </GridViewColumn.Header>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Grid>
                                    <TextBlock
                                        Text="{Binding Name}" />
                                </Grid>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Width="80" DisplayMemberBinding="{Binding Size}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Size" Tag="Size"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                    <GridViewColumn Width="140" DisplayMemberBinding="{Binding Modified}">
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Content="Modified" Tag="Modified"/>
                        </GridViewColumn.Header>
                    </GridViewColumn>

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace ExplorerModoki4;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    // キー入力のディレイ
    string _typeBuffer = "";
    DateTime _lastTypeTime = DateTime.MinValue;

    const int TypeTimeoutMs = 700;

    // ファイルの一覧、ListViewのItemSource
    public ObservableCollection<FileItem> Items {get; set;} = [];
    // 選択中のItem、バインド元
    FileItem? _selectedItem;
    public FileItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }
    }
    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;

        this.Loaded += MainWindow_Loaded;
    }

    // ファイルの一覧取得(検証用)
    void MainWindow_Loaded(object? sender, RoutedEventArgs e)
    {
        Items.Clear();

        string path = @"C:\Users\karet\Pictures";

        var dir = new DirectoryInfo(path);

        // フォルダ
        foreach (var d in dir.GetDirectories())
        {
            Items.Add(new FileItem
            {
                Name = d.Name,
                IsFolder = true,
                Size = 0,
                Modified = d.LastWriteTime
            });
        }

        // ファイル
        foreach (var f in dir.GetFiles())
        {
            Items.Add(new FileItem
            {
                Name = f.Name,
                IsFolder = false,
                Size = f.Length,
                Modified = f.LastWriteTime
            });
        }

    }

    void ListView_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        // Rename 中は TextBox に任せる
        if (Items.Any(x => x.IsEditing))
            return;

        var now = DateTime.Now;

        // 一定時間空いたらリセット
        if ((now - _lastTypeTime).TotalMilliseconds > TypeTimeoutMs)
            _typeBuffer = "";

        _lastTypeTime = now;
        _typeBuffer += e.Text;

        // Explorer 風:フォルダ優先 + 名前順で検索
        var view = CollectionViewSource.GetDefaultView(Items);

        var match = view.Cast<FileItem>()
                        .FirstOrDefault(x =>
                            x.Name.StartsWith(
                                _typeBuffer,
                                StringComparison.OrdinalIgnoreCase));

        if (match != null)
        {
            SelectedItem = match;
            ListView.ScrollIntoView(match);
        }

        e.Handled = true;
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

1行目選択→キーボードの「D」を押す

「desktop.ini」へジャンプする。

さいごに

思いつく機能を施策してみましたが、

これらを一つのプロジェクトにまとめて、

きちんと機能するかは後日挑戦したいと思います。

コメント