前回からの変更点
- キャッシュファイル機能の追加による2回目以降の高速化
- サムネイルの生成スレッドからListViewItemのThubを更新
ソースコード
Helpers
ファイル名:Helpers\AppPathHelper.cs
// アプリケーション固有の保存パスを一元管理する Helper。
using System.Reflection;
namespace Maywork.WPF.Helpers;
public static class AppPathHelper
{
/// アプリケーション名。
public static string AppName { get; } = GetDefaultAppName();
// ------------------------------------------------------------
// static ctor
// ------------------------------------------------------------
static AppPathHelper()
{
EnsureDirectories();
}
// ------------------------------------------------------------
// public paths
// ------------------------------------------------------------
/// 設定ファイル・ユーザー設定用(APPDATA)
public static string Roaming =>
System.IO.Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData),
AppName);
/// キャッシュ・ログ・一時データ用(LOCALAPPDATA)
public static string Local =>
System.IO.Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData),
AppName);
public static string SettingsFile =>
System.IO.Path.Combine(Roaming, "settings.json");
public static string CacheDir =>
System.IO.Path.Combine(Local, "cache");
public static string LogDir =>
System.IO.Path.Combine(Local, "log");
public static string TempDir =>
System.IO.Path.Combine(Local, "temp");
// ------------------------------------------------------------
// helpers
// ------------------------------------------------------------
private static string GetDefaultAppName()
{
var asm = Assembly.GetEntryAssembly();
return asm?.GetName().Name ?? "Application";
}
/// <summary>
/// 必要なディレクトリをすべて作成する。
/// </summary>
public static void EnsureDirectories()
{
System.IO.Directory.CreateDirectory(Roaming);
System.IO.Directory.CreateDirectory(Local);
System.IO.Directory.CreateDirectory(CacheDir);
System.IO.Directory.CreateDirectory(LogDir);
System.IO.Directory.CreateDirectory(TempDir);
}
}
/*
【APPDATA (Roaming)】
・ユーザー設定ファイル
・UIレイアウト
・履歴情報
・ユーザー辞書など
※PC移行・ドメイン環境ではローミング対象。
【LOCALAPPDATA (Local)】
・キャッシュデータ
・ログファイル
・一時ファイル
・画像サムネイル
※削除されても再生成可能なデータを保存する。
*/
ファイル名:Helpers\Dialog.cs
using Microsoft.Win32;
namespace Maywork.WPF.Helpers;
static class Dialogs
{
public static string? OpenDir(string dir)
{
var dialog = new OpenFolderDialog
{
Title = "フォルダを選択してください",
InitialDirectory = dir,
Multiselect = false
};
return dialog.ShowDialog() == true ? dialog.FolderName : null;
}
}
ファイル名:Helpers\IconHelper.cs
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Maywork.WPF.Helpers;
public static class IconHelper
{
// ------------------------------------------------------------
// Win32 API
// ------------------------------------------------------------
[DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo(
string pszPath,
uint dwFileAttributes,
out SHFILEINFO psfi,
uint cbFileInfo,
uint uFlags);
[DllImport("Shell32.dll", EntryPoint = "#727")]
private static extern int SHGetImageList(
int iImageList,
ref Guid riid,
out IImageList ppv);
[DllImport("User32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
// ------------------------------------------------------------
// Structs & Constants
// ------------------------------------------------------------
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
private const uint SHGFI_SYSICONINDEX = 0x00004000;
private const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
private const int SHIL_SMALL = 0; // 16x16
private const int SHIL_LARGE = 1; // 32x32
private const int SHIL_EXTRALARGE = 2; // 48x48
private const int SHIL_JUMBO = 4; // 256x256
// ------------------------------------------------------------
// COM Interface (Order is critical!)
// ------------------------------------------------------------
[ComImport]
[Guid("46EB5926-582E-4017-9FDF-E8998DAA0950")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IImageList
{
int Add(IntPtr hbmImage, IntPtr hbmMask, ref int pi);
int ReplaceIcon(int i, IntPtr hicon, ref int pi);
int SetOverlayImage(int iImage, int iOverlay);
int Replace(int i, IntPtr hbmImage, IntPtr hbmMask);
int AddMasked(IntPtr hbmImage, uint crMask, ref int pi);
int Draw(IntPtr pimldp); // 引数は簡易化
int Remove(int i);
int GetIcon(int i, int flags, out IntPtr picon); // 8番目のメソッド
// 以降のメソッドは今回使用しないため省略可能
}
// ------------------------------------------------------------
// Public API
// ------------------------------------------------------------
public static ImageSource GetIconImageSource(string path, int size = 32)
{
var info = new SHFILEINFO();
bool isDirectory = System.IO.Directory.Exists(path);
uint attr = isDirectory ? (uint)0x10 : (uint)0x80; // 0x10: Directory, 0x80: Normal File
uint flags = SHGFI_SYSICONINDEX | SHGFI_USEFILEATTRIBUTES;
// 第一引数に path を渡しても、USEFILEATTRIBUTES があれば属性優先になります。
SHGetFileInfo(path, attr, out info, (uint)Marshal.SizeOf(info), flags);
// 2. サイズに応じたイメージリストの種類を選択
int listType = size switch
{
<= 16 => SHIL_SMALL,
<= 32 => SHIL_LARGE,
<= 48 => SHIL_EXTRALARGE,
_ => SHIL_JUMBO
};
// 3. IImageList 取得
Guid guid = typeof(IImageList).GUID;
int hr = SHGetImageList(listType, ref guid, out var imageList);
if (hr == 0 && imageList != null)
{
hr = imageList.GetIcon(info.iIcon, 0x00000001, out IntPtr hIcon);
if (hr == 0 && hIcon != IntPtr.Zero)
{
try
{
// BitmapSizeOptions を指定せず、元のサイズを維持して作成
var bmp = Imaging.CreateBitmapSourceFromHIcon(
hIcon,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
bmp.Freeze();
return bmp;
}
finally
{
DestroyIcon(hIcon); // ハンドル漏れ防止
}
}
}
// 何も取れなければ透明1x1
var wb = new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgra32, null);
wb.Freeze();
return wb;
}
}
/*
public static class IconHelper
{
// SHGetFileInfoでHICONを取得してWPFのImageSourceへ変換
[DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes,
out SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
private const uint SHGFI_ICON = 0x000000100;
private const uint SHGFI_LARGEICON = 0x000000000; // 32x32
private const uint SHGFI_SHELLICONSIZE = 0x000000004;
[DllImport("User32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
public static ImageSource GetIconImageSource(string path)
{
var info = new SHFILEINFO();
IntPtr result = SHGetFileInfo(path, 0, out info, (uint)Marshal.SizeOf(info),
SHGFI_ICON | SHGFI_LARGEICON | SHGFI_SHELLICONSIZE);
if (result != IntPtr.Zero && info.hIcon != IntPtr.Zero)
{
var img = Imaging.CreateBitmapSourceFromHIcon(
info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
DestroyIcon(info.hIcon);
img.Freeze();
return img;
}
// 何も取れなければ透明1x1
var wb = new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgra32, null);
wb.Freeze();
return wb;
}
}
*/
ファイル名:Helpers\ImageHelper.cs
using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Maywork.WPF.Helpers;
static class ImageHelper
{
public static BitmapSource Load(string path)
{
BitmapImage bmp = new();
bmp.BeginInit();
bmp.UriSource = new Uri(path, UriKind.Absolute);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bmp.EndInit();
bmp.Freeze(); // ← UIスレッド外OK
return bmp;
}
public static BitmapSource CreateThumb(BitmapSource src)
{
const int MaxSize = 256;
const double Dpi = 96.0;
// 縦横比維持スケール
double scaleX = (double)MaxSize / src.PixelWidth;
double scaleY = (double)MaxSize / src.PixelHeight;
double scale = Math.Min(scaleX, scaleY);
int width = (int)Math.Round(src.PixelWidth * scale);
int height = (int)Math.Round(src.PixelHeight * scale);
// 描画用ビジュアル
var dv = new DrawingVisual();
using (var dc = dv.RenderOpen())
{
dc.DrawImage(
src,
new Rect(0, 0, width, height)
);
}
// DPI=96で描き直す
var rtb = new RenderTargetBitmap(
width,
height,
Dpi,
Dpi,
PixelFormats.Pbgra32
);
rtb.Render(dv);
rtb.Freeze();
return rtb;
}
public static void SaveJpeg(BitmapSource bmp, string savePath)
{
var encoder = new JpegBitmapEncoder
{
QualityLevel = 85 // 0–100
};
encoder.Frames.Add(BitmapFrame.Create(bmp));
using var fs = new FileStream(
savePath,
FileMode.Create,
FileAccess.Write,
FileShare.None
);
encoder.Save(fs);
}
}
ファイル名:Helpers\ThumbLoader.cs
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Media.Imaging;
namespace Maywork.WPF.Helpers;
public static class ThumbLoader
{
// 文字列からMD5ハッシュ文字列へ変換
public static string ToMd5(string text)
{
using var md5 = MD5.Create();
byte[] bytes = Encoding.UTF8.GetBytes(text);
byte[] hash = md5.ComputeHash(bytes);
// 32文字の16進文字列へ
return Convert.ToHexString(hash).ToLowerInvariant();
}
// キャッシュのKeyを生成
public static string CreateCacheKey(string path)
{
var info = new FileInfo(path);
string modified =
info.LastWriteTime.ToString("yyyyMMddHHmmss");
string size = info.Length.ToString();
return ToMd5($"{path}-{modified}-{size}");
}
public static BitmapSource LoadOrCreateThumb(
string path,
out string? cachePath)
{
var dir = AppPathHelper.CacheDir;
var file = CreateCacheKey(path) + ".jpg";
cachePath = Path.Combine(dir, file);
if (File.Exists(cachePath))
{
return ImageHelper.Load(cachePath);
}
var src = ImageHelper.Load(path);
var thumb = ImageHelper.CreateThumb(src);
thumb.Freeze(); // ← 重要
return thumb;
}
}
ファイル名:Helpers\ThumbnailHelper.cs
using System.Windows.Media.Imaging;
namespace Maywork.WPF.Helpers;
public static class ThumbnailHelper
{
// サムネイル用画像の読み込み
public static BitmapSource LoadImageThumb(
string file,
int thumbSize = 256)
{
var bmp = new BitmapImage();
bmp.BeginInit();
try
{
// ファイルロック回避
bmp.CacheOption = BitmapCacheOption.OnLoad;
// デコード段階で縮小(超重要)
bmp.DecodePixelWidth = thumbSize;
// または Height でもOK(片方だけ指定)
bmp.UriSource = new Uri(file, UriKind.Absolute);
bmp.EndInit();
}
catch
{
// 何もしない。
}
finally
{
bmp.Freeze(); // 非UIスレッドOK
}
return bmp;
}
}
ファイル名:Helpers\ViewModelBase.cs
// 汎用的な ViewModel 基底クラス実装。
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Maywork.WPF.Helpers;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
ファイル名:MainWindow.xaml
<Window x:Class="WpfFileManager2.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:WpfFileManager2"
mc:Ignorable="d"
Title="FileManager" Height="800" Width="400">
<TabControl
x:Name="TabHost"
TabStripPlacement="Bottom"/>
</Window>
ファイル名:MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace WpfFileManager2;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// タブのアイテムを動的に追加するループ
TabItem? firstTab = null;
foreach (var factory in TabViewRegistry.Tabs)
{
var tabview = factory();
var tab = new TabItem
{
Header = tabview.Title,
Content = tabview // ← UserControl
};
if (firstTab is null)
{
firstTab = tab;
}
TabHost.Items.Add(tab);
}
TabHost.SelectedItem = firstTab;
}
}
ファイル名:TabViewRegistry.cs
using WpfFileManager2.Views;
namespace WpfFileManager2;
public static class TabViewRegistry
{
public static IReadOnlyList<Func<ITabView>> Tabs { get; } =
[
() => new FileManagerView(),
];
}
Utilities
ファイル名:Utilities\SubProcUtil.cs
using System.Diagnostics;
namespace Maywork.Utilities;
public static class SubProcUtil
{
public static bool Launch(string path)
{
bool result = true;
try
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
}
catch
{
result = false;
}
return result;
}
}
ファイル名:Views\FileManagerFileItem.cs
using System.Collections.Concurrent;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;
using Maywork.WPF.Helpers;
using System.Security.Cryptography;
using System.Text;
using System.Diagnostics;
public class FileManagerFileItem : ViewModelBase
{
public string Path { get; }
public string DisplayName { get; }
public ImageSource Icon { get; }
BitmapSource? _thumb;
public BitmapSource? Thumb
{
get => _thumb;
set
{
if (_thumb == value) return;
_thumb = value;
OnPropertyChanged(nameof(Thumb));
}
}
// コンストラクタ
public FileManagerFileItem(string path, string displayName, ImageSource icon, BitmapSource? thumb)
{
Path = path;
DisplayName = displayName;
Icon = icon;
Thumb = thumb;
}
// ファクトリメソッド
public static FileManagerFileItem FromPath(string path)
{
var name = System.IO.Path.GetFileName(path);
var icon = GetIconCached(path);
var thumb = GetJumboIconCached(path) as BitmapSource;
var item = new FileManagerFileItem(path, name, icon, thumb);
return item;
}
// ------------------------------
// icon cache
// ------------------------------
private static readonly ConcurrentDictionary<string, ImageSource> _iconCache
= new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, ImageSource> _jumboIconCache
= new(StringComparer.OrdinalIgnoreCase);
//private static readonly ConcurrentDictionary<string, BitmapSource> _thumbCache
// = new(StringComparer.OrdinalIgnoreCase);
// ------------------------------
// icon logic
// ------------------------------
private static ImageSource GetIconCached(string path)
{
// フォルダ
bool isDirectory = string.IsNullOrEmpty(System.IO.Path.GetExtension(path));
if (isDirectory)
{
return _iconCache.GetOrAdd(
"<DIR>",
_ => IconHelper.GetIconImageSource(path, 16));
}
var ext = System.IO.Path.GetExtension(path);
// 拡張子なし
if (string.IsNullOrEmpty(ext))
ext = "<NOEXT>";
// exe は個別アイコン
if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
return IconHelper.GetIconImageSource(path, 16);
}
// 拡張子単位でキャッシュ
return _iconCache.GetOrAdd(ext, _ =>
{
return IconHelper.GetIconImageSource(path, 16);
});
}
private static ImageSource GetJumboIconCached(string path)
{
// フォルダ
bool isDirectory = string.IsNullOrEmpty(System.IO.Path.GetExtension(path));
if (isDirectory)
{
return _jumboIconCache.GetOrAdd(
"<DIR>",
_ => IconHelper.GetIconImageSource(path, 256));
}
var ext = System.IO.Path.GetExtension(path);
// 拡張子なし
if (string.IsNullOrEmpty(ext))
ext = "<NOEXT>";
// exe は個別アイコン
if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
return IconHelper.GetIconImageSource(path, 256);
}
// 拡張子単位でキャッシュ
return _jumboIconCache.GetOrAdd(ext, _ =>
{
return IconHelper.GetIconImageSource(path, 256);
});
}
// 文字列からMD5ハッシュ文字列へ変換
public static string ToMd5(string text)
{
using var md5 = MD5.Create();
byte[] bytes = Encoding.UTF8.GetBytes(text);
byte[] hash = md5.ComputeHash(bytes);
// 32文字の16進文字列へ
return Convert.ToHexString(hash).ToLowerInvariant();
}
// キャッシュのKeyを生成
public static string CreateCacheKey(string path)
{
var info = new FileInfo(path);
string modified =
info.LastWriteTime.ToString("yyyyMMddHHmmss");
string size = info.Length.ToString();
return ToMd5($"{path}-{modified}-{size}");
}
// サムネ画像のロード
public static BitmapSource LoadImageThumb(string path)
{
var dir = AppPathHelper.CacheDir;
var file = CreateCacheKey(path) + ".jpg";
var chashFile = System.IO.Path.Combine(dir, file);
if (File.Exists(chashFile))
{
return ImageHelper.Load(chashFile);
}
var src = ImageHelper.Load(path);
var thumb = ImageHelper.CreateThumb(src);
var parent = System.IO.Path.GetDirectoryName(path);
if (parent != dir)
{
Task.Run(()=>ImageHelper.SaveJpeg(thumb, chashFile));
}
return thumb;
}
}
ファイル名:Views\FileManagerView.xaml
<UserControl x:Class="WpfFileManager2.Views.FileManagerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions>
<!-- メニューバー -->
<RowDefinition Height="Auto"/>
<!-- アドレスバー -->
<RowDefinition Height="Auto"/>
<!-- メイン: -->
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- メニューバー -->
<Menu
Grid.Row="0">
<Menu.FontSize>16</Menu.FontSize>
<MenuItem Header="ファイル(_F)">
<MenuItem Header="開く(_O)" x:Name="MenuOpen"/>
</MenuItem>
<MenuItem Header="表示(_V)">
<MenuItem Header="一覧(_L)" x:Name="MenuListMode" />
<MenuItem Header="サムネ(_T)" x:Name="MenuThumbMode" />
<Separator />
<MenuItem Header="サムネクリア(_C)" x:Name="MenuThumbClear" />
</MenuItem>
</Menu>
<!-- アドレスバー -->
<Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="0,1,0,1">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <!-- 上へ -->
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/> <!-- カレントディレクトリ -->
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/> <!-- 更新 -->
</Grid.ColumnDefinitions>
<Button
x:Name="UpButton"
Grid.Column="0"
Padding="4,4"
Content="↑"
Background="Transparent"
FontSize="16"/>
<!-- カレントディレクトリ -->
<DockPanel Grid.Column="2" LastChildFill="True">
<TextBlock Text="アドレス:" VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox
x:Name="CurrentDirTextBox"
VerticalContentAlignment="Center"
Padding="6,3"/>
</DockPanel>
<Button
x:Name="UpdateButton"
Grid.Column="4"
Padding="10,4"
Content="↵"
Background="Transparent"
FontSize="16"/>
</Grid>
</Border>
<!-- 一覧 -->
<ListView Grid.Row="2"
x:Name="List"
Visibility="Collapsed"
Margin="8"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter
Property="FontSize"
Value="14"/>
<Setter
Property="Padding"
Value="4"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView AllowsColumnReorder="False">
<GridViewColumn
Header="アイコン"
Width="48">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image
Source="{Binding Icon}"
Width="24"
Height="24"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn
Header="名前"
Width="300"
DisplayMemberBinding="{Binding DisplayName}"/>
</GridView>
</ListView.View>
</ListView>
<!-- サムネイル -->
<ListView Grid.Row="2"
x:Name="Thumb"
Visibility="Visible"
Margin="8"
SelectionMode="Single"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter
Property="FontSize"
Value="14"/>
<Setter
Property="Padding"
Value="4"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Border
BorderThickness="4"
CornerRadius="6"
Background="Transparent"
Margin="5">
<StackPanel Margin="5" Width="256">
<Image
Source="{Binding Thumb}"
Width="256" Height="256"
Stretch="Uniform" />
<TextBlock
Text = "{Binding DisplayName}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</UserControl>
ファイル名:Views\FileManagerView.xaml.cs
using System.Diagnostics;
using System.IO;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Input;
using Maywork.WPF.Helpers;
using Maywork.Utilities;
namespace WpfFileManager2.Views;
public partial class FileManagerView : UserControl, ITabView
{
public string Title => "ファイルマネージャ";
// ドラッグ開始を判定するための開始位置
private Point _startPoint;
private string _currentDir = "";
public FileManagerView()
{
InitializeComponent();
List.MouseDoubleClick += async (_, __) => await ListItemDoubleClickAsync();
Thumb.MouseDoubleClick += async (_, __) => await ListItemDoubleClickAsync();
UpButton.Click += async (_, __) => await GoParentDirAsync();
UpdateButton.Click += async(_, __) => await UpdateDirAsync();
MenuOpen.Click += async (_, __) => await DirSelectDialogAsync();
MenuListMode.Click += (_, __) => ChangeListMode();
MenuThumbMode.Click += (_, __) => ChangeThumMode();
MenuThumbClear.Click += async (_, __) => ThumbClear();
List.PreviewMouseLeftButtonDown += ListView_PreviewMouseLeftButtonDown;
List.PreviewMouseMove += ListView_PreviewMouseMove;
Thumb.PreviewMouseLeftButtonDown += ListView_PreviewMouseLeftButtonDown;
Thumb.PreviewMouseMove += ListView_PreviewMouseMove;
Loaded += async (_, __) =>
{
await ChangeCurrentDir(@"C:\Users\karet\Pictures");
};
}
// カレントディレクトリの変更処理
async Task ChangeCurrentDir(string path)
{
if (_currentDir == path) return;
_currentDir = path;
CurrentDirTextBox.Text = _currentDir;
var sw = Stopwatch.StartNew();
var entries = Directory.EnumerateFileSystemEntries(path)
.Where(file =>
{
try
{
var attr = File.GetAttributes(file);
return (attr & (FileAttributes.Hidden | FileAttributes.System)) == 0;
}
catch
{
return false;
}
})
.Select(file => FileManagerFileItem.FromPath(file))
.ToList();
List.ItemsSource = entries;
Thumb.ItemsSource = entries;
if (Thumb.Visibility == Visibility.Visible)
{
bool hasImage =
entries.Any(x =>
{
string ext = Path.GetExtension(x.Path).ToLowerInvariant();
return ext == ".jpeg"
|| ext == ".jpg"
|| ext == ".png"
|| ext == ".webp";
});
if (hasImage)
await UpdateThumbAsync();
}
sw.Stop();
Debug.WriteLine($"ChangeCurrentDir: {sw.ElapsedMilliseconds} ms {path}");
}
// ダブルクリック処理
async Task ListItemDoubleClickAsync()
{
var item = List.SelectedItem as FileManagerFileItem;
if (item is null)
{
item = Thumb.SelectedItem as FileManagerFileItem;
}
if (item is null) return;
//bool isDirectory = string.IsNullOrEmpty(Path.GetExtension(item.Path));
bool isDirectory = Directory.Exists(item.Path);
if (isDirectory)
{
await ChangeCurrentDir(item.Path);
}
else
{
await Task.Run(()=>SubProcUtil.Launch(item.Path));
}
}
// 親ディレクトリへ
async Task GoParentDirAsync()
{
var parent = Path.GetDirectoryName(_currentDir);
if (parent is null || parent == "") return;
await ChangeCurrentDir(parent);
}
// カレントディレクトリをアドレスバーの入力で更新
async Task UpdateDirAsync()
{
string path = CurrentDirTextBox.Text;
if (!Directory.Exists(path)) return;
await ChangeCurrentDir(path);
}
// ディレクトリ選択ダイアログ
async Task DirSelectDialogAsync()
{
string? newPath = Dialogs.OpenDir(_currentDir);
if (newPath is null) return;
await ChangeCurrentDir(newPath);
}
// サムネモードへ変更
void ChangeThumMode()
{
if (Thumb.Visibility == Visibility.Visible) return;
Thumb.Visibility = Visibility.Visible;
List.Visibility = Visibility.Hidden;
}
// 一覧モードへ変更
void ChangeListMode()
{
if (List.Visibility == Visibility.Visible) return;
Thumb.Visibility = Visibility.Hidden;
List.Visibility = Visibility.Visible;
}
// サムネイル更新
async Task UpdateThumbAsync()
{
string path = _currentDir;
var sw = Stopwatch.StartNew();
// ① UIスレッドでコピー
var items = List.ItemsSource
.Cast<FileManagerFileItem>()
.ToList();
await Parallel.ForEachAsync(items, async (item, ct) =>
{
await Task.Run(() =>
{
string file = item.Path;
string ext = Path.GetExtension(file).ToLower();
if (ext is ".jpeg" or ".jpg" or ".png" or ".webp")
{
var bmp = FileManagerFileItem.LoadImageThumb(file);
Dispatcher.Invoke(() =>
{
item.Thumb = bmp;
});
}
}, ct);
});
sw.Stop();
Debug.WriteLine($"UpdateThumb: {sw.ElapsedMilliseconds} ms {path}");
}
private void ListView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// マウスを押した位置を記録
_startPoint = e.GetPosition(null);
}
private void ListView_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
Point mousePos = e.GetPosition(null);
Vector diff = _startPoint - mousePos;
if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance)
return;
var listView = sender as ListView;
if (listView == null)
return;
// ★ マウス直下の要素を取得
DependencyObject? obj =
listView.InputHitTest(e.GetPosition(listView)) as DependencyObject;
// ★ ListViewItem を親方向に探索
while (obj != null && obj is not ListViewItem)
{
obj = VisualTreeHelper.GetParent(obj);
}
// ScrollBar 等はここで弾かれる
if (obj is not ListViewItem item)
return;
// 選択アイテム取得
if (item.DataContext is not FileManagerFileItem file)
return;
if (string.IsNullOrEmpty(file.Path))
return;
// ドラッグ開始
string[] paths = { file.Path };
DataObject data = new(DataFormats.FileDrop, paths);
DragDrop.DoDragDrop(listView, data, DragDropEffects.Copy);
}
// サムネイルのクリア
void ThumbClear()
{
string dir = AppPathHelper.CacheDir;
foreach (var file in Directory.EnumerateFiles(dir, "*.jpg"))
{
File.Delete(file);
}
}
}
ファイル名:Views\ITabView.cs
namespace WpfFileManager2.Views;
public interface ITabView
{
string Title { get; }
}
ファイル名:WpfFileManager2.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>

コメント