前回の記事のサンプルコードを一つのプロジェクトにまとめました。


WPFでExplorerライクなUIを作成する。
ファイルマネージャの作成のため、Explorerを真似たUIの操作をWPFで再現してみたいと思います。F2で名前の変更ファイル名:Converter\BoolToVisibilityConverter.csusing System.Glob...
ソースコード
ファイル名:Converters.cs
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace ExplorerModoki5;
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;
}
public class InverseBoolToVisibilityConverter : IValueConverter
{
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;
}
ファイル名:ExplorerModoki5.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 ExplorerModoki5;
public class FileItem : INotifyPropertyChanged
{
string _name = "";
string _originalName = "";
bool _isEditing;
public string Name
{
get => _name;
set { if (_name != value) { _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;
void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
ファイル名:MainWindow.xaml
<Window x:Class="ExplorerModoki5.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ExplorerModoki5"
Title="ExplorerModoki5" Height="520" Width="900">
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVis"/>
<local:InverseBoolToVisibilityConverter x:Key="InvBoolToVis"/>
<!-- FileItem 用メニュー -->
<ContextMenu x:Key="FileItemMenu">
<MenuItem Header="名前の変更" Click="Menu_Rename_Click"/>
<Separator/>
<MenuItem Header="削除(UIのみ)" Click="Menu_Delete_Click"/>
</ContextMenu>
<!-- 空白用メニュー -->
<ContextMenu x:Key="BackgroundMenu">
<MenuItem Header="更新(ダミー)" Click="Menu_Refresh_Click"/>
<Separator/>
<MenuItem Header="並び替え(ダミー)" Click="Menu_SortDummy_Click"/>
</ContextMenu>
</Window.Resources>
<Grid>
<ListView x:Name="ListView1"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
KeyDown="ListView_KeyDown"
PreviewTextInput="ListView_PreviewTextInput"
ContextMenuOpening="ListView_ContextMenuOpening"
GridViewColumnHeader.Click="GridViewColumnHeader_Click">
<ListView.View>
<GridView>
<!-- Name (編集対応) -->
<GridViewColumn Width="260">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Name">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Name"/>
<TextBlock x:Name="Arrow"
Margin="4,0,0,0"
Text=""
FontSize="10"/>
</StackPanel>
</GridViewColumnHeader>
</GridViewColumn.Header>
<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>
<!-- Size -->
<GridViewColumn Width="100" DisplayMemberBinding="{Binding Size}">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Size" Tag="Size"/>
</GridViewColumn.Header>
</GridViewColumn>
<!-- Modified -->
<GridViewColumn Width="160" DisplayMemberBinding="{Binding Modified}">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Modified" Tag="Modified"/>
</GridViewColumn.Header>
</GridViewColumn>
<!-- IsFolder (デバッグ用に表示しても良い) -->
<GridViewColumn Width="90" DisplayMemberBinding="{Binding IsFolder}">
<GridViewColumn.Header>
<GridViewColumnHeader Content="IsFolder" Tag="IsFolder"/>
</GridViewColumn.Header>
</GridViewColumn>
</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.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace ExplorerModoki5;
public partial class MainWindow : Window, INotifyPropertyChanged
{
public ObservableCollection<FileItem> Items { get; } = [];
FileItem? _selectedItem;
public FileItem? SelectedItem
{
get => _selectedItem;
set { if (_selectedItem != value) { _selectedItem = value; OnPropertyChanged(); } }
}
// --- ソート状態 ---
string? _sortProperty = null;
ListSortDirection _sortDirection = ListSortDirection.Ascending;
// --- タイプジャンプ ---
string _typeBuffer = "";
DateTime _lastTypeTime = DateTime.MinValue;
const int TypeTimeoutMs = 700;
public MainWindow()
{
InitializeComponent();
DataContext = this;
Loaded += (_, _) =>
{
// お好みの初期ディレクトリ
LoadDirectory(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
// 初期ソート(Explorer っぽく Name)
_sortProperty = nameof(FileItem.Name);
_sortDirection = ListSortDirection.Ascending;
ApplySort();
};
}
// ----------------------------
// ファイル一覧取得(取得のみ)
// ----------------------------
void LoadDirectory(string path)
{
Items.Clear();
try
{
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
});
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "LoadDirectory");
}
}
// ----------------------------
// KeyDown: F2 / Delete など
// ----------------------------
void ListView_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.F2)
{
BeginRename();
e.Handled = true;
return;
}
if (e.Key == Key.Delete)
{
// Rename中はTextBoxに任せる(Explorer風)
if (Items.Any(x => x.IsEditing))
return;
DeleteSelectedUiOnly();
e.Handled = true;
return;
}
}
void BeginRename()
{
if (SelectedItem == null) return;
// 画面外ならまず可視化(仮想化対策)
ListView1.ScrollIntoView(SelectedItem);
// 他の編集を終了
foreach (var item in Items)
item.IsEditing = false;
SelectedItem.IsEditing = true;
}
void DeleteSelectedUiOnly()
{
if (SelectedItem == null) return;
var index = Items.IndexOf(SelectedItem);
Debug.WriteLine($"[Delete(UI)] {SelectedItem.Name}");
Items.Remove(SelectedItem);
if (Items.Count == 0)
{
SelectedItem = null;
return;
}
// Explorer風:次、なければ前
if (index < Items.Count)
SelectedItem = Items[index];
else
SelectedItem = Items[^1];
}
// --------------------------------
// Rename: TextBox Visible→Focus
// --------------------------------
void RenameTextBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is not TextBox tb) return;
if (!tb.IsVisible) return;
// フォーカス競合に勝つ
tb.Dispatcher.BeginInvoke(new Action(() =>
{
tb.Focus();
Keyboard.Focus(tb);
tb.SelectAll();
}), DispatcherPriority.Input);
}
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;
return;
}
if (e.Key == Key.Escape)
{
// キャンセル
item.Name = item.OriginalName;
item.IsEditing = false;
e.Handled = true;
return;
}
}
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}");
}
}
// ----------------------------
// ContextMenu: Item/空白で切替
// ----------------------------
void ListView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (sender is not ListView lv) return;
var element = e.OriginalSource as DependencyObject;
var lvi = element != null
? ItemsControl.ContainerFromElement(lv, element) as ListViewItem
: null;
if (lvi?.DataContext is FileItem item)
{
// 右クリックで選択移動(Explorer風)
lv.SelectedItem = item;
// Rename中はメニュー無効でもOK(お好み)
if (item.IsEditing)
{
e.Handled = true;
return;
}
lv.ContextMenu = (ContextMenu)FindResource("FileItemMenu");
}
else
{
lv.ContextMenu = (ContextMenu)FindResource("BackgroundMenu");
}
}
void Menu_Rename_Click(object sender, RoutedEventArgs e) => BeginRename();
void Menu_Delete_Click(object sender, RoutedEventArgs e) => DeleteSelectedUiOnly();
void Menu_Refresh_Click(object sender, RoutedEventArgs e)
=> MessageBox.Show("更新(ダミー)");
void Menu_SortDummy_Click(object sender, RoutedEventArgs e)
=> MessageBox.Show("並び替え(ダミー)※ヘッダークリックでソートします");
// ----------------------------
// Header click sort
// ----------------------------
void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
{
// Rename中は無視(任意)
if (Items.Any(x => x.IsEditing))
return;
if (e.OriginalSource is not GridViewColumnHeader header)
return;
if (header.Tag is not string propertyName)
return;
// 同じ列→トグル、別列→昇順
if (_sortProperty == propertyName)
{
_sortDirection =
_sortDirection == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
}
else
{
_sortProperty = propertyName;
_sortDirection = ListSortDirection.Ascending;
}
ApplySort();
}
void ApplySort()
{
if (_sortProperty == null) return;
var view = CollectionViewSource.GetDefaultView(Items);
view.SortDescriptions.Clear();
// Explorer風:フォルダ優先
view.SortDescriptions.Add(
new SortDescription(nameof(FileItem.IsFolder), ListSortDirection.Descending));
view.SortDescriptions.Add(
new SortDescription(_sortProperty, _sortDirection));
view.Refresh();
Debug.WriteLine($"[Sort] {_sortProperty} {_sortDirection}");
UpdateSortArrows();
}
// ----------------------------
// Type jump (keyboard search)
// ----------------------------
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;
var view = CollectionViewSource.GetDefaultView(Items);
var match = view.Cast<FileItem>()
.FirstOrDefault(x =>
x.Name.StartsWith(_typeBuffer, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
SelectedItem = match;
ListView1.ScrollIntoView(match);
}
e.Handled = true;
}
void UpdateSortArrows()
{
// ListView 内のすべての GridViewColumnHeader を探す
foreach (var header in FindVisualChildren<GridViewColumnHeader>(ListView1))
{
if (header.Content is StackPanel sp &&
sp.Children.OfType<TextBlock>().Skip(1).FirstOrDefault() is TextBlock arrow)
{
arrow.Text = ""; // 一旦クリア
}
// 現在のソート列だけ矢印を付ける
if (header.Tag as string == _sortProperty)
{
if (header.Content is StackPanel sp2 &&
sp2.Children.OfType<TextBlock>().Skip(1).FirstOrDefault() is TextBlock arrow2)
{
arrow2.Text = _sortDirection == ListSortDirection.Ascending ? "▲" : "▼";
}
}
}
}
static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)
where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T t)
yield return t;
foreach (var x in FindVisualChildren<T>(child))
yield return x;
}
}
// ----------------------------
// INotifyPropertyChanged
// ----------------------------
public event PropertyChangedEventHandler? PropertyChanged;
void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

コメント