C#のWinFormsでファイルマネージャを作る。「試作編」

C# コンピュータ
C#

少しずつ調べていた機能がだいぶ溜まったので、以前から作ろうと思っていたファイルマネージャ(劣化版エクスプローラー)を作成したいと思います。
アプリケーションのファイルの一覧からGIMPなどの画像加工のアプリケーションに画像ファイルをドラッグアンドドロップすることを目的としています。

機能・特徴

  • 縦長でプレビューは下段
  • カレントディレクトリの移動は下層はファイルの一覧のフォルダ項目、上層はアドレスバーのコンボボックス
  • フォルダのお気に入り機能(追加ボタンでアドレスバーのコンボボックスに追加)
  • ほかのアプリケーションへドラッグアンドドロップ

追加予定

  • メニューのファイル項目を機能させる。(今のところダミー)
  • 更新ボタン
  • 一つ上の階層へボタン
  • ファイルアイコン

問題点

  • リストビューのアイテムをマウスでのクリックの動作が怪しい。イベントハンドラの見直し
  • ファイルマネージャなのにファイルのコピー、移動、削除が無い

ソースコード

FileListViewEntity.cs

namespace ImageList2;

public class FileListViewEntity
{
    public string FullPath {get; set;} = "";
    public string Name
    {
        get
        {
            return Path.GetFileName(FullPath);
        }
    }
}

FileSystemManager.cs

using System.Diagnostics;

namespace ImageList2;

public class FileSystemManager
{
    public string CurrentDirectory = @"";
    public string FavoritesPath = @".\favorites.txt";

    List<string> favoritesList = new()
    {
    };

    public FileSystemManager()
    {
        CurrentDirectory = Directory.GetCurrentDirectory();
        loadFavoriteFile();
    }
    void loadFavoriteFile()
    {
        if (File.Exists(FavoritesPath) == false) return;
        using var sr = new StreamReader(FavoritesPath);
        while (sr.Peek() != -1)
        {
            var path = sr.ReadLine();
            if (Directory.Exists(path))
            {
                favoritesList.Add(path);
            }
        }
        CurrentDirectory = favoritesList[favoritesList.Count-1];
        favoritesList.Remove(CurrentDirectory);
    }
    public void SaveFavoriteFile()
    {
        using var sw = new StreamWriter(FavoritesPath, false);
        foreach(var path in favoritesList)
        {
            sw.WriteLine(path);
        }
        sw.WriteLine(CurrentDirectory);
    }
    static public bool CheckDirectoryPath(string path)
    {
        return Directory.Exists(path);
    }
    /// <summary>
    /// ドライブの一覧を返す
    /// </summary>
    /// <returns>ドライブとカレントディレクトリの階層リスト</returns>
    public List<string> GetDriveListAndBreadcrumb()
    {
        List<string> result = new();
        string currentRoot = Path.GetPathRoot(CurrentDirectory) ?? "";

        foreach(var drive in Directory.GetLogicalDrives())
        {
            result.Add(drive);

            if (drive.ToUpper() == currentRoot.ToUpper())
            {
                string tmp = CurrentDirectory;
                Stack<string> tmps = new();
                while(currentRoot.ToUpper() != tmp.ToUpper())
                {
                    tmps.Push(tmp);
                    tmp = Path.GetDirectoryName(tmp) ?? currentRoot;
                }
                result.AddRange(tmps);
            }
        }
        result.AddRange(favoritesList);
        return result;
    }
    // カレントディレクトリのファイルの一覧を取得
    public List<FileListViewEntity> GetCurrentDirectoryFileList()
    {
        List<FileListViewEntity> list = new();
        foreach(var path in Directory.EnumerateFileSystemEntries(CurrentDirectory))
        {
            var fullPath = Path.GetFullPath(path);
            if (fullPath is null) continue;
            var e = new FileListViewEntity()
            {
                FullPath = fullPath,
            };
            list.Add(e);
        }
        return list;
    }
    // お気に入りに登録されているか
    public bool IsFavorite()
    {
        var path = CurrentDirectory;

        return (favoritesList.IndexOf(path) >= 0);

    }
    // お気に入りに追加
    public void AddFavorite()
    {
        if (IsFavorite()) return;

        var path = CurrentDirectory;
        favoritesList.Add(path);
    }
    // お気に入りから削除
    public void RemoveFavorite()
    {
        if (IsFavorite() == false) return;

        var path = CurrentDirectory;
        favoritesList.Remove(path);
    }

}

Form1.AddressBar.cs

using System.ComponentModel;
using System.Xml.Schema;

namespace ImageList2;

partial class Form1
{
   // アドレスバーの更新
    void updateAddressBar()
    {
        addressBar.BeginUpdate();
        addressBar.Items.Clear();
        foreach(var f in fsManager.GetDriveListAndBreadcrumb())
        {
            addressBar.Items.Add(f);
        }
        int i = addressBar.Items.IndexOf(fsManager.CurrentDirectory);
        addressBar.SelectedIndex = i;
        addressBar.EndUpdate();
    }
    // 選択されているIndexが変更された
    void addressBar_SelectedIndexChanged(object? sender, EventArgs e)
    {
        if (addressBar.SelectedItem is null) return;
        if (addressBar.SelectedIndex < 0) return;
        // 選択された項目からディレクトリを取り出し
        var dir = addressBar.SelectedItem.ToString() ?? "";
        // イベントの無限ループ防止
        if (dir == "" || fsManager.CurrentDirectory == dir ) return;

        // カレントディレクトリ変更
        fsManager.CurrentDirectory = dir;
        // コンボボックスの更新
        updateAddressBar();
        // ファイルの一覧を更新
        updateFileListView();
    }
    // バリデーション
    void addressBar_Validating(object? sender, CancelEventArgs e)
    {
        var dir = addressBar.Text;
        if (!FileSystemManager.CheckDirectoryPath(dir))
        {
            MessageBox.Show($"{dir}は無効なパス。", "えらー");
            addressBar.Text = fsManager.CurrentDirectory;
            e.Cancel = true;
            return;
        }
    }
    // バリデーション完了
    void addressBar_Validated (object? sender, EventArgs e)
    {
        fsManager.CurrentDirectory = addressBar.Text;
        updateAddressBar();
        // ファイルの一覧を更新
        updateFileListView();
    }
} // class

Form1.cs

using System.Diagnostics;

namespace ImageList2;

public partial class Form1 : Form
{
    FileSystemManager fsManager = new();
    public Form1()
    {
        InitializeComponent();
        this.Load += Form1_Load;
        this.FormClosing += Form1_FormClosing;

        addFavoriteMenuItem.Click += addFavoriteMenuItem_Click;
        removeFavoriteMenuItem.Click += removeFavoriteMenuItem_Click;

        addressBar.SelectedIndexChanged += addressBar_SelectedIndexChanged;
        addressBar.Validating += addressBar_Validating;
        addressBar.Validated += addressBar_Validated;

        fileListView.DoubleClick += fileListView_DoubleClick;
        fileListView.SelectedIndexChanged += fileListView_SelectedIndexChanged;
        fileListView.MouseDown += fileListView_MouseDown;
    }
    // フォームLoadイベントハンドラ
    void Form1_Load(object? sender, EventArgs e)
    {

        // ファイル一覧の初期化
        initializeFileListView();

        var strip = new MenuStrip();
        
        strip.Items.AddRange(new ToolStripItem[]
        {
            fileGroupMenuItem,
            addFavoriteMenuItem,
            removeFavoriteMenuItem,
        });
        toolBar.Items.Add(addressBar);
        mainPanel.Panel1.Controls.Add(fileListView);
        mainPanel.Panel2.Controls.Add(preViewPicBox);
        this.Controls.AddRange(new Control[]
        {
            mainPanel,
            toolBar,
            strip,
        });
        this.MainMenuStrip = strip;

        // アドレスバーの更新
        updateAddressBar();
        // ファイルの一覧を更新
        updateFileListView();
    }
    // フォームが閉じられるイベント
    void Form1_FormClosing(object? sender, EventArgs e)
    {
        fsManager.SaveFavoriteFile();
    }
}

Form1.Designer.cs

namespace ImageList2;

partial class Form1
{
    /// <summary>
    ///  Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    ///  Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    ///  Required method for Designer support - do not modify
    ///  the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        int h = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height - 180;
        this.components = new System.ComponentModel.Container();
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(800, h);
        this.Text = "Form1";
    }

    #endregion
    ToolStripMenuItem fileGroupMenuItem = new()
    {
        Text = "ファイル",
    };
    ToolStripMenuItem addFavoriteMenuItem = new()
    {
        Text = "追加",
    };
    ToolStripMenuItem removeFavoriteMenuItem = new()
    {
        Text = "削除",
    };
    // ツールバー
    ToolStrip toolBar = new()
    {
        Dock = DockStyle.Top,
    };
    // アドレスバー
    ToolStripComboBox addressBar = new()
    {
        BackColor = Color.AliceBlue,
        Width = 600,
        Height = 45,
    };
    // ファイルの一覧
    ListView fileListView = new()
    {
        Dock = DockStyle.Fill,
        View = View.Details,
    };
    // プレビュー画像
    PictureBox preViewPicBox = new()
    {
        SizeMode = PictureBoxSizeMode.Zoom,
        Dock = DockStyle.Fill,
    };
    // メインパネル
    SplitContainer mainPanel = new()
    {
        Dock = DockStyle.Fill,
        Orientation = Orientation.Horizontal,
    };
}

Form1.fileListView.cs

using System.Diagnostics;
using System.Reflection.Metadata.Ecma335;

namespace ImageList2;

partial class Form1
{
    // ファイルの一覧を初期化
    void initializeFileListView()
    {
        Size icoSize = new (32, 32);
        ImageList imglist = new()
        {
            ImageSize = icoSize,
        };
        Font fnt = new Font("Segoe UI emoji", 9.0f);

        // ディレクトリアイコン
        var dirIcon = new Bitmap(icoSize.Width, icoSize.Height);
        using var dirg = Graphics.FromImage(dirIcon);
        dirg.DrawString("\U0001F4C1", fnt, new SolidBrush(Color.Black), 0,0, StringFormat.GenericDefault);
        imglist.Images.Add("Directory", dirIcon);

        // ファイルアイコン
        var fileIcon = new Bitmap(icoSize.Width, icoSize.Height);
        using var fileg = Graphics.FromImage(fileIcon);
        fileg.DrawString("\U0001F4C4", fnt, new SolidBrush(Color.Black), 0,0, StringFormat.GenericDefault);
        imglist.Images.Add("File", fileIcon);

        fileListView.SmallImageList = imglist;

        // 項目の追加
        fileListView.Columns.Add("名前", 360, HorizontalAlignment.Left);
    }
    // ファイルの一覧を更新
    void updateFileListView()
    {
        fileListView.BeginUpdate();
        fileListView.Items.Clear();
        foreach(var e in fsManager.GetCurrentDirectoryFileList())
        {
            ListViewItem item = new()
            {
                Text = e.Name,
                ImageKey = File.Exists(e.FullPath) ? "File" : "Directory",
            };
            fileListView.Items.Add(item);
        }
        fileListView.EndUpdate();
    }
    // ファイルの一覧のダブルクリックイベントハンドラ
    void fileListView_DoubleClick(Object? sender, EventArgs e)
    {
        if (fileListView.SelectedItems.Count <= 0) return;

        var item = fileListView.SelectedItems[0];

        if (item.ImageKey == "Directory")
        {
            var newPath = Path.Join(fsManager.CurrentDirectory, item.Text);
            if (newPath != fsManager.CurrentDirectory)
            {
                // ここにオブザーバーが欲しい
                fsManager.CurrentDirectory = newPath;
                // アドレスバーの更新
                updateAddressBar();
                // ファイルの一覧を更新
                updateFileListView();
            }
        }
    }
    // ファイル一覧の選択インデックスが変更された
    async void fileListView_SelectedIndexChanged(Object? sender, EventArgs e)
    {
         if (fileListView.SelectedItems.Count <= 0) return;

        var item = fileListView.SelectedItems[0];

        if (item.ImageKey == "File")
        {
            var filePath = Path.Join(fsManager.CurrentDirectory, item.Text);
            preViewPicBox.Image = await GraphicHelper.LoadImageFileAsync(filePath);
        }
   }
    void fileListView_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 = Path.Join(fsManager.CurrentDirectory, result.Text);
        if (Directory.Exists(path)) return;
        

        var effect = DragDropEffects.Copy;
        string[] paths = {path};
        IDataObject data = new DataObject(DataFormats.FileDrop, paths);
        lv.DoDragDrop(data, effect);
    }
}

Form1.Menu.cs

using System.ComponentModel;
using System.Xml.Schema;

namespace ImageList2;

partial class Form1
{
    // 追加
    void addFavoriteMenuItem_Click(object? sender, EventArgs e)
    {
        fsManager.AddFavorite();
        updateAddressBar();
    }

    // 削除
    void removeFavoriteMenuItem_Click(object? sender, EventArgs e)
    {
        fsManager.RemoveFavorite();
        updateAddressBar();
    }
} // class

GraphicHelper.cs

using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace ImageList2;

public static class GraphicHelper
{
    static List<string>? filenameExtentions;
    public static List<string> GetFilenameExtentions()
    {
        if (filenameExtentions is not null) return filenameExtentions;

        filenameExtentions = new();

        var decs = System.Drawing.Imaging.ImageCodecInfo.GetImageDecoders();
        foreach(var dec in decs)
        {
            var extstr = dec.FilenameExtension;
            var exts = extstr?.Split(";");
            if (exts is not null)
            {
                foreach(var ext in exts)
                {
                    filenameExtentions.Add(ext.Replace("*", ""));
                }
            }
            else
            {
                if (extstr is not null)
                {
                    filenameExtentions.Add(extstr.Replace("*", ""));
                }
            }
        }
        return filenameExtentions;
    }
    public static bool IsSupported(string path)
    {
        var ext = Path.GetExtension(path).ToUpper();
        if (ext is null) return false;
        
        var exts = GetFilenameExtentions();
        return (exts.IndexOf(ext) >= 0);
    }
    public static async Task<Bitmap?> LoadImageFileAsync(string filePath)
    {
        string ext = Path.GetExtension(filePath).ToUpper();

        if (IsSupported(filePath) == false)
        {
            return null;
        }

        return await Task.Run(() =>
        {
            using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            Bitmap bitmap = new Bitmap(fs);
            return bitmap;
        });
    }
};

実行


初回起動はカレントディレクトリ。2回目以降は前回終了したディレクトリで開始。(縦長になっていない)


アドレスバーのコンボボックスを展開すると、ドライブの一覧とカレントディレクトリの階層が表示されますので、選択することでディレクトリを移動します。また、ファイルの一覧のディレクトリをダブルクリックするとそのディレクトリへ移動します。


画像ファイルを選択するとプレビューが表示されます。
メニューの追加ボタンでカレントディレクトリをお気に入り登録。削除ボタンで登録を解除します。


お気に入り登録したディレクトリはアドレスバーのコンボボックスの下部に追加されます。

感想

ソースコードが長くなりましたので、ファイル(クラス)をいくつかに分割しています。
このアプリケーションはファイル操作がメインとなりますので、FileSystemManager.csにフォームの見た目以外の機能を集約させました。また、フォームを構成するコントロールごとにForm1.csも分割しています。

コメント