WPF サムネイル表示アプリ試作メモ5「仕様変更・サムネイル生成の並列処理」

コンピュータ

前回の記事から色々機能を増やしたところ、

怪しい挙動が発生していたので、

全面的にやり直してみました。

ついでに、サムネイル生成部分をParallel.ForEachAsyncで並列処理にしてみました。

ソースコード

プロジェクト

ファイル名: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>

Helpers

ファイル名: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\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;
    }

}

ファイル名: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;

public class FileManagerFileItem
{
    public string Path { get; }
    public string DisplayName { get; }
    public ImageSource Icon { get; }
    public BitmapSource? Thumb {get; set;}
    public BitmapSource? TempThumb {get; set;}

    // コンストラクタ
    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 bmp = ThumbnailHelper.LoadImageThumb(path);
        var key = CreateCacheKey(path);
        // MD5でキャッシュ
        return _thumbCache.GetOrAdd(key, _ =>
        {
            return ThumbnailHelper.LoadImageThumb(path);
        });
    }

}

ファイル名: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();
        MenuThumbUpdate.Click += async (_, __) => await UpdateThumbAsync();

        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;
        if (Thumb.Visibility == Visibility.Visible)
        {
            Thumb.ItemsSource = entries;
            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));

        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")
                {
                    item.TempThumb = FileManagerFileItem.LoadImageThumb(file);
                }
            }, ct);
        });
        // ③ UIスレッドで反映
        foreach (var item in items)
        {
            if (item.TempThumb != null)
            {
                item.Thumb = item.TempThumb;
                item.TempThumb = null;
            }
        }

        Thumb.ItemsSource = items;

        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);
    }
}

ファイル名:Views\ITabView.cs

namespace WpfFileManager2.Views;

public interface ITabView
{
    string Title { get; }
}

ファイル名: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>

ファイル名: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="サムネ更新(_U)" x:Name="MenuThumbUpdate" />
            </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>

実行例

サムネイル作成に時間がかかるので、表示が遅れます。

コメント