C#よく使うフォルダをブックマークして画像ファイルをサムネイル表示するアプリ

コンピュータ
個人的にフォルダ単位に画像ファイルを管理しているのですが、動画の素材用の画像を保存してある特定のフォルダに頻繁にアクセスします。その場合画像の内容が目視で確認できるようにサムネイル表示するようにしています。基本的にエクスプローラーで行っている作業ですが、専用のツールがあれば作業効率が高まるのではないかと思い試作してみたいと思います。

ツールの機能

  • 画像ファイルのサムネイル表示(エクスプローラーの画像サイズより大きめで)
  • サムネイル画像をドラックアンドドロップで別アプリケーションで開く
  • 画像フォルダの選択
  • よく使う画像フォルダのブックマーク機能

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms

ソースコード

ファイル名:Form1.cs

using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.VisualBasic;

namespace ImageList1;

public partial class Form1 : Form
{
    string _path = @"C:\Users\karet\Pictures";
    string _iniPath = @".\ImageList1.txt";
    List<ListViewItem> _listitem = new();
    
    ListView _listView = new()
    {
        Padding = new Padding(128),
        Dock = DockStyle.Fill,
        OwnerDraw = true,
        VirtualMode = true,
        LargeImageList = new ImageList
        {
            ImageSize = new Size(Thumbnail1.Width, Thumbnail1.Height),
        },
    };
    MenuStrip _menuStrip = new()
    {
        Dock = DockStyle.Top,
    };
    ToolStripMenuItem _menuItem = new()
    {
        Text = "実行",
    };
    ToolStripMenuItem _menuItemFolderSelect = new()
    {
        Text = "フォルダ選択"
    };
    ToolStripMenuItem _menuItemFolderEntory = new()
    {
        Text = "フォルダ登録"
    };
    ToolStripComboBox toolStripComboBox1 = new()
    {
        Size = new Size(292, 25),
    };
    public Form1()
    {
        InitializeComponent();

        //_menuStrip.Items.Add(_menuItem);
        _menuStrip.Items.AddRange(new ToolStripItem[]{_menuItemFolderSelect, _menuItemFolderEntory, toolStripComboBox1});
        Controls.AddRange(new Control[] {_listView, _menuStrip, });

        _menuItem.Click += _menuItem_Click;
        _menuItemFolderSelect.Click += _menuItemFolderSelect_Click;
        _menuItemFolderEntory.Click += _menuItemFolderEntory_Click;
        _listView.RetrieveVirtualItem += _listView_RetrieveVirtualItem;
        _listView.DrawItem += _listView_DrawItem;
        _listView.MouseDown += _listView_MouseDown;
        this.Load += Form1_Load;
        this.FormClosing += Form1_FormClosing;

        if (File.Exists(_iniPath)) {
            using var sr = new StreamReader(_iniPath);
            while (sr.Peek() > -1) {
                string? line = sr.ReadLine();
                if (line is not null) {
                    toolStripComboBox1.Items.Add(line);
                }
            }
            if (toolStripComboBox1.Items.Count > 0) {
                toolStripComboBox1.SelectedIndex = 0;
                this._path = (string)toolStripComboBox1.Items[0];
            }
        }
        toolStripComboBox1.SelectedIndexChanged += toolStripComboBox1_SelectedIndexChanged;
        Size = new Size(1600, 2000);
    }
    void Form1_Load(object? s, EventArgs e)
    {
        if (Path.Exists(this._path)) {
            Update_ListView();
        }
    }
    void _listView_MouseDown(object? sender, MouseEventArgs e)
    {        
        if (sender is null) return;
        if (e.Button != MouseButtons.Left) return;

        var lv = (ListView)sender;
        var result = lv.GetItemAt(e.X, e.Y);
        if (result is null) return;
        string path = result.Text;
        Debug.Print(path);
        var effect = DragDropEffects.Copy;
        string[] paths = {path};
        IDataObject data = new DataObject(DataFormats.FileDrop, paths);
        lv.DoDragDrop(data, effect);
    }
    void Form1_FormClosing(object? s, FormClosingEventArgs e)
    {
        using var sw = new StreamWriter(_iniPath);
        for (int i = 0; i < toolStripComboBox1.Items.Count; i++) {
            string line = (string)toolStripComboBox1.Items[i];
            sw.WriteLine(line);
        }
    }
    void toolStripComboBox1_SelectedIndexChanged(object? sender, EventArgs e)
    {
        int i = toolStripComboBox1.SelectedIndex;
        var newPath = (string)toolStripComboBox1.Items[i];

        // newPathが存在しない場合一覧から削除するか問い合わせる。
        if (Path.Exists(newPath) == false) {
            string msg = String.Format("「{0}」が存在しないため登録を解除します。", newPath);
            MessageBox.Show(msg, "通知");
            toolStripComboBox1.Items.RemoveAt(i);
            return;
        }

        if (newPath != this._path) {
            this.Text = newPath;
            this._path = newPath;
            Update_ListView();
        }

    }
    void _menuItem_Click(Object? sender, EventArgs e)
    {
        _menuItem.Enabled = false;

        Update_ListView();

        _menuItem.Enabled = true;
    }
    void Update_ListView()
    {
        // 画像ファイルの一覧を取得、拡張子で絞り込み、サムネイルファイルを除く
        IEnumerable<string> imgFiles = Directory.EnumerateFiles(_path, "*.*", SearchOption.TopDirectoryOnly)
            .Where(x => Regex.IsMatch(x, @"\.(png|jp(e)?g|gif|bmp)$", RegexOptions.IgnoreCase))
            .Where(x => !Regex.IsMatch(x, @"_thumbnail\.", RegexOptions.IgnoreCase));

        int w = _listView.Size.Width / Thumbnail1.Width;
        int h = _listView.Size.Height / Thumbnail1.Height;


        // リストビューの更新開始
        _listView.BeginUpdate();

        if (_listitem.Count > 0) _listitem.Clear();
        foreach(var file in imgFiles)
        {
            _listitem.Add(new ListViewItem(file));
        }
        _listView.VirtualListSize = _listitem.Count;


        // リストビューの更新終了
        _listView.EndUpdate();

    }
    void _menuItemFolderSelect_Click(Object? sender, EventArgs e)
    {
        FolderBrowserDialog dialog = new()
        {
            Description = "フォルダを指定してください。",
            RootFolder = Environment.SpecialFolder.Desktop,
            SelectedPath = _path,
            ShowNewFolderButton = false,
        };
        if (dialog.ShowDialog(this) == DialogResult.OK) {
            this.Text = dialog.SelectedPath;
            this._path = dialog.SelectedPath;
            Update_ListView();
        }
    }
    void _menuItemFolderEntory_Click(object? sender, EventArgs e)
    {
        //MessageBox.Show("登録", "a");
        if ( toolStripComboBox1.Items.Contains(this._path) == false) {
            string msg = String.Format("「{0}」を登録しますか?", this._path);
            if (MessageBox.Show(msg, "登録", MessageBoxButtons.OKCancel) == DialogResult.OK) {
                toolStripComboBox1.Items.Add(this._path);
            }
        }
    }
    void _listView_RetrieveVirtualItem(Object? sender, RetrieveVirtualItemEventArgs e)
    {
        if (e.Item == null) e.Item = _listitem[e.ItemIndex];
    }
    void _listView_DrawItem(Object? sender, DrawListViewItemEventArgs e)
    {
        string filename = e.Item.Text;
        Image? thumbnail = Thumbnail1.LoadImage(filename);
        if (thumbnail == null)
            return;

        Rectangle imagerect = new Rectangle(new Point(e.Bounds.X + ((e.Bounds.Width - thumbnail.Width) / 2), e.Bounds.Y), new Size(thumbnail.Width, thumbnail.Height));

        e.DrawDefault = false;
        e.DrawBackground();
        e.Graphics.DrawImage(thumbnail, imagerect);

        var stringFormat = new StringFormat() {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center,
        };
        e.Graphics.DrawString(e.Item.Text, _listView.Font, Brushes.Black, new RectangleF(e.Bounds.X, e.Bounds.Y + imagerect.Height + 5, e.Bounds.Width, e.Bounds.Height - imagerect.Height - 5), stringFormat);
        e.DrawFocusRectangle();
    }
}//class

ファイル名:Thumbnail1.cs

using System.Diagnostics;
public class Thumbnail1
{
    static public readonly int Width = 384;
    static public readonly int Height = 384;

    static Object _lockObj = new();
    static Dictionary<string, Image> _thumbnailCache = new();

    static public Bitmap CreateThumbnail(string filename)
    {
        // サムネイルファイルの作成
        string thumbnailPath = "";
        string? dir = Path.GetDirectoryName(filename);
        string? bn = Path.GetFileNameWithoutExtension(filename);
        string? ext = Path.GetExtension(filename);
        if (dir is not null && bn is not null && ext is not null) {
            thumbnailPath = Path.Join(dir, bn+"_thumbnail"+ext);
            if (Path.Exists(thumbnailPath)) {
                // 更新日時の確認
                var ttime = File.GetLastWriteTime(thumbnailPath);
                var otime = File.GetLastWriteTime(filename);
                if (ttime > otime) {
                    // 既に作成されたサムネイルファイルを読み込んで戻す
                    using var tfs = new FileStream(thumbnailPath, FileMode.Open, FileAccess.Read);
                    return (Bitmap)Bitmap.FromStream(tfs);
                } else {
                    // サムネイルの更新日>元画像の更新日の場合サムネイルファイルを削除
                    File.Delete(thumbnailPath);
                }

            }
        }


        using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);

        Bitmap canvas = new Bitmap(Width, Height);
        using var original = Bitmap.FromStream(fs);
        using var g = Graphics.FromImage(canvas);
        g.Clear(Color.White);

        double fw = (double)Width / (double)original.Width;
        double fh = (double)Height / (double)original.Height;

        double scale = Math.Min(fw, fh);

        int w2 = (int)(original.Width * scale);
        int h2 = (int)(original.Height * scale);

        g.DrawImage(original, (Width-w2)/2, (Height-h2)/2, w2, h2);

        // サムネイルを保存
        canvas.Save(thumbnailPath);
        var attr = File.GetAttributes(thumbnailPath);
        // サムネイルファイルを隠しファイルにに変更
        File.SetAttributes(thumbnailPath, attr | FileAttributes.Hidden);
        
        return canvas;
    }

    public static Image? LoadFile(string filename)
    {
        if (!File.Exists(filename)) return null;
        var thumbnail = CreateThumbnail(filename);
        lock(_lockObj)
        {
            if (!_thumbnailCache.ContainsKey(filename))
                _thumbnailCache.Add(filename, thumbnail);
        }
        return thumbnail;
    }

    public static Image? LoadImage(string filename)
    {
        if (_thumbnailCache.ContainsKey(filename))
        {
            return _thumbnailCache[filename];
        }
        return LoadFile(filename);
    }
}

実行

dotnet run

感想

とりあえず目的の機能は全て盛り込みました。
画像ファイルの選択とドラックアンドドロップの動作が怪しいです。

リストビューのアイテムをドラッグする場合、ドラッグするアイテムを指定する必要があります。
MouseDownイベントでListView.SelectedIndicesで現在選択されいる拾えば良いかと試してみました。
上手く行く場合もありますが、異なるファイルがドロップされる場合があります。
想像するに、リストビューのアイテムが選択される前にMouseDownイベントが発生していると思われます。

次に選択アイテムが選択された際発生するイベントSelectedIndexChangeでドラッグ処理を行ってみました。
確実に選択されたアイテムがドロップされるようになりました。問題解決かと思いましたが、既に選択されたアイテムはドラック出来ない致命的な問題が発生します。SelectedIndexChangeですので選択アイテムがChangeしていないので当然です。

仕方が無いのでListView.SelectedIndicesプロパティを使うことをあきらめて、MouseDownイベントでマウス座標からListViewのアイテムを指定する方法にしてみました。動作は問題なさそうですが、より正しいやり方がありそうな感じがします。

作って実際使ってみると、自分が行う作業手順に最適化されるので、作業効率の向上が実感できます。自分専用のカスタマイズですので汎用性は皆無ですが、自分が使うツールを作るプログラミングは成果が実感できて楽しいです。

コメント