前回作成した内容で使ってみたのですが、目的が画像や動画の選択ですので、ファイルの一覧で画像の内容が目視出来たほうが良いことに気が付きました。
ついでにGIMPの.xcfやPhothoShopの.psd及び動画の.aviや.mp4などの内容も表示してくれると都合がよいので対応しました。
SDKのバージョンはNET8.0にしました。こちらのバージョンの.dllからアイコンを取得する関数が使えるようになりました。
プロジェクトの作成
dotnet new winforms --name ImageList2 -f net8.0
cd ImageList2
dotnet add package System.Data.SQLite
dotnet add package System.Drawing.Common
dotnet add package Magick.NET.Core
dotnet add package Magick.NET.SystemDrawing
dotnet add package Magick.NET-Q8-AnyCPU
dotnet add package OpenCvSharp4
dotnet add package OpenCvSharp4.Extensions
dotnet add package OpenCvSharp4.runtime.win
ソースコード
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;
reloadMenuItem.Click += reloadMenuItem_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;
fileListView.RetrieveVirtualItem += fileListView_RetrieveVirtualItem;
fileListView.DrawItem += fileListView_DrawItem;
}
// フォームLoadイベントハンドラ
void Form1_Load(object? sender, EventArgs e)
{
// ファイル一覧の初期化
initializeFileListView();
var strip = new MenuStrip();
strip.Items.AddRange(new ToolStripItem[]
{
fileGroupMenuItem,
addFavoriteMenuItem,
removeFavoriteMenuItem,
reloadMenuItem,
});
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 = "削除",
};
ToolStripMenuItem reloadMenuItem = 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.LargeIcon,
OwnerDraw = true,
VirtualMode = true,
};
// プレビュー画像
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;
using System.Windows.Forms.Design;
namespace ImageList2;
partial class Form1
{
static Size largeIconSize = new(256, 256); // new(384, 384)
ImageList largeImageList = new()
{
ImageSize = largeIconSize,
};
// ファイルの一覧を初期化
void initializeFileListView()
{
fileListView.LargeImageList = largeImageList;
// 項目の追加
fileListView.Columns.Add("名前", 360, HorizontalAlignment.Left);
}
// ファイルの一覧を更新
List<ListViewItem> listViewItemList = new();
void updateFileListView()
{
fileListView.BeginUpdate();
listViewItemList.Clear();
fileListView.Items.Clear();
largeImageList.Images.Clear();
foreach(var e in fsManager.GetCurrentDirectoryFileList())
{
ListViewItem item = new()
{
Text = e.Name,
};
listViewItemList.Add(item);
}
fileListView.VirtualListSize = listViewItemList.Count;
fileListView.EndUpdate();
}
// ファイルの一覧のダブルクリックイベントハンドラ
void fileListView_DoubleClick(Object? sender, EventArgs e)
{
var items = fileListView.SelectedIndices;
if (items.Count <= 0) return;
int i = items[0];
var newPath = Path.Join(fsManager.CurrentDirectory, listViewItemList[i].Text);
if (Directory.Exists(newPath) && newPath != fsManager.CurrentDirectory)
{
// ここにオブザーバーが欲しい
fsManager.CurrentDirectory = newPath;
// アドレスバーの更新
updateAddressBar();
// ファイルの一覧を更新
updateFileListView();
}
}
// ファイル一覧の選択インデックスが変更された
void fileListView_SelectedIndexChanged(Object? sender, EventArgs e)
{
var items = fileListView.SelectedIndices;
if (items.Count <= 0) return;
int i = items[0];
var filePath = Path.Join(fsManager.CurrentDirectory, listViewItemList[i].Text);
if (!File.Exists(filePath)) return;
preViewPicBox.Image?.Dispose();
preViewPicBox.Image = GraphicHelper.LoadImageFile(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);
}
void fileListView_RetrieveVirtualItem(Object? sender, RetrieveVirtualItemEventArgs e)
{
if (e.Item == null) e.Item = listViewItemList[e.ItemIndex];
}
void fileListView_DrawItem(Object? sender, DrawListViewItemEventArgs e)
{
string filename = Path.Join(fsManager.CurrentDirectory, e.Item.Text);
var thumbnail = GraphicHelper.GetThumnailImage(filename, largeIconSize.Width, largeIconSize.Height);
if (thumbnail is null)
{
thumbnail = new Bitmap(largeIconSize.Width, largeIconSize.Height);
}
Rectangle imagerect = new Rectangle(new Point(e.Bounds.X + ((e.Bounds.Width - largeIconSize.Width) / 2), e.Bounds.Y), new Size(largeIconSize.Width, largeIconSize.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, fileListView.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();
}
}
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();
}
void reloadMenuItem_Click(object? sender, EventArgs e)
{
updateFileListView();
}
} // class
GraphicHelper.cs
using System.Diagnostics;
using System.Runtime.CompilerServices;
using ImageMagick;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System.Drawing.Imaging;
using System.IO.Compression;
using System.ComponentModel;
using System.Data.Common;
using System.Data.SQLite;
using System.Data.Entity.Core.EntityClient;
namespace ImageList2;
public static class GraphicHelper
{
// データベースファイル
const string DB_FILE = @".\thumnail.db";
// ZIPファイル
const string ZIP_FILE = @".\thumnail.zip";
// コネクションオブジェクトの生成
static SQLiteConnection conn = new();
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("*", ""));
}
}
}
filenameExtentions.Add(".xcf".ToUpper());
filenameExtentions.Add(".psd".ToUpper());
filenameExtentions.Add(".avi".ToUpper());
filenameExtentions.Add(".mp4".ToUpper());
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 Bitmap? LoadImageFile(string filePath)
{
string ext = Path.GetExtension(filePath).ToUpper();
if (IsSupported(filePath) == false)
{
return null;
}
if (ext == ".AVI" || ext == ".MP4")
{
using var mat = new Mat();
using var vc = new VideoCapture(filePath);
if (vc.Read(mat))
{
if (mat.IsContinuous())
{
return BitmapConverter.ToBitmap(mat);;
}
}
return null;
}
if (ext == ".XCF" || ext == ".PSD")
{
using var magick = new MagickImage(filePath);
return magick.ToBitmap();
}
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
Bitmap bitmap = new Bitmap(fs);
return bitmap;
}
private static Bitmap? LoadFileIcon(string filePath, int w, int h)
{
if (Directory.Exists(filePath))
{
string? systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
if (systemRoot is not null)
{
string dllPath = Path.Join(systemRoot, @"System32\SHELL32.dll");
if (File.Exists(dllPath))
{
var icon = Icon.ExtractIcon (dllPath, 3, w);
if (icon is not null)
{
return icon.ToBitmap();
}
}
}
return null;
}
using Bitmap? bmp = LoadImageFile(filePath);
if (bmp is null)
{
var icon2 = Icon.ExtractAssociatedIcon(filePath);
if (icon2 is null)
{
return null;
}
return icon2.ToBitmap();
}
Bitmap canvas = new Bitmap(w, h);
Graphics g = Graphics.FromImage(canvas);
g.FillRectangle(new SolidBrush(Color.White), 0, 0, w, h);
float fw = (float)w / (float)bmp.Width;
float fh = (float)h / (float)bmp.Height;
float scale = Math.Min(fw, fh);
fw = bmp.Width * scale;
fh = bmp.Height * scale;
g.DrawImage(bmp, (w - fw) / 2, (h - fh) / 2, fw, fh);
g.Dispose();
return canvas;
}
/*
データベースと接続
*/
static void ConnectDatabase()
{
// 接続文字列をセット
conn.ConnectionString = $"Data Source = {DB_FILE}";
// データベースを開く
conn.Open();
// コマンドオブジェクトを作成
using var cmd = new SQLiteCommand(conn);
// テーブルを作成
cmd.CommandText = "CREATE TABLE IF NOT EXISTS thumbnail (id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT, lastmodified TEXT);";
cmd.ExecuteNonQuery();
// インデックスを作成
cmd.CommandText = "CREATE UNIQUE INDEX IF NOT EXISTS filenameindex ON thumbnail(filename, lastmodified);";
cmd.ExecuteNonQuery();
}
/*
データベースから切断
*/
static void DisconnectDatabase()
{
conn.Close();
}
/*
thumbnailテーブルからデータを取得
*/
static bool GetThumbnailOne(ref ThumbnailEntity record)
{
bool result = false;
using var cmd = new SQLiteCommand(conn);
string? filename = record.FileName;
string? lastmodified = record.LastModified;
cmd.CommandText = $"SELECT id, filename, lastmodified FROM thumbnail where filename = '{filename}' and lastmodified = '{lastmodified}'";
using var rec = cmd.ExecuteReader();
if (rec.Read()) {
result = true;
record.ID = (long)rec["id"];
record.FileName = (string)rec["filename"];
record.LastModified = (string)rec["lastmodified"];
}
return result;
}
/*
thumbnailテーブルへデータを追加・更新
戻り値:true 新規、false、既存
*/
static bool UpdateThumbnailOne(ref ThumbnailEntity record)
{
// データベースのサムネイル情報を取得
ThumbnailEntity old = new()
{
FileName = record.FileName,
LastModified = record.LastModified,
};
if (GetThumbnailOne(ref old)) {
// サムネイル情報がある場合
// idをセットして
record.ID = old.ID;
// リターン
return false;
}
using var cmd = new SQLiteCommand(conn);
// サムネイル情報がない場合・追加
cmd.CommandText = $"INSERT INTO thumbnail (filename, lastmodified) values ('{record.FileName}','{record.LastModified}')";
cmd.ExecuteNonQuery();
// 更新結果を取得
return GetThumbnailOne(ref record);
}
/*
画像を指定サイズに縮小する
*/
/*
static System.Drawing.Bitmap ResizeImage(System.Drawing.Image src, int width=384, int height=384)
{
// 戻り値用のBitmapオブジェクトの生成
var result = new System.Drawing.Bitmap(width, height);
// グラフィックオブジェクトを取得
using var g = Graphics.FromImage(result);
// 縦横の比率から縮小率を計算
double fw = (double)width / (double)src.Width;
double fh = (double)height / (double)src.Height;
double scale = Math.Min(fw, fh);
// 縮小後の幅と高さを計算
int w2 = (int)(src.Width * scale);
int h2 = (int)(src.Height * scale);
// 縮小画像を中央に描画
g.DrawImage(src, (width-w2)/2, (height-h2)/2, w2, h2);
return result;
}
*/
/*
zipファイルに画像を追加
*/
static void AddBitmapToZip(string zipFile, ref Bitmap bmp, string entryName)
{
// zipファイルを更新モードで開く
using var zip = ZipFile.Open(zipFile, ZipArchiveMode.Update);
// zipファイル内のファイル(エントリー)を作成
var ns = Path.GetFileNameWithoutExtension(entryName) + ".png";
var entry = zip.CreateEntry(ns, CompressionLevel.NoCompression); // 無圧縮
// エントリーへ書き込むストリームを作成
using var fs = entry.Open();
// ビットマップをzipファイルへ保存
bmp.Save(fs, ImageFormat.Png);
}
/*
zipファイルから画像を取得
*/
static Image? GetBitmapFromZip(string zipFile, string entryName)
{
// zipファイルを更新モードで開く
using var zip = ZipFile.Open(zipFile, ZipArchiveMode.Read);
// zipファイル内のファイル(エントリー)を作成
var ns = Path.GetFileNameWithoutExtension(entryName) + ".png";
var entry = zip.GetEntry(ns);
if (entry is null ) return null;
// エントリーへ書き込むストリームを作成
using var fs = entry.Open();
// ビットマップを読み込み
return Bitmap.FromStream(fs);
}
/*
サムネイル画像を取得
*/
public static Image? GetThumnailImage(string filename, int w=384, int h=384)
{
// ファイルの情報
var fileInfo = new FileInfo(filename);
// データベースへ接続
ConnectDatabase();
ThumbnailEntity tbl = new()
{
FileName = fileInfo.FullName,
LastModified = fileInfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
};
// データベースの更新
if (UpdateThumbnailOne(ref tbl) == false) {
// 既存
string entryName = tbl.ID.ToString();
var img = GetBitmapFromZip(ZIP_FILE, entryName);
// 切断
DisconnectDatabase();
return img;
} else {
// 新規追加
string entryName = tbl.ID.ToString();
var smallBmp = LoadFileIcon(filename, w, h);
if (smallBmp is not null)
{
AddBitmapToZip(ZIP_FILE, ref smallBmp, entryName);
}
// 切断
DisconnectDatabase();
return smallBmp;
}
}
};
ThumbnailEntity.cs
public class ThumbnailEntity
{
public long ID = 0;
public string? FileName;
public string? LastModified;
}
サムネイル画像(上部)が大きいため、プレビュー画像(下部)の意味がなくなってしまいました。
いろいろな画像や動画ファイルに対応する為にGraphicHelper.csを変更したため、かなりごちゃついてきました。
Bitmapオブジェクトの加工などのサブルーチンをを置く場所のつもりでいたのですが、ファイルからBitmapオブジェクトを生成をこちらに記述したところ、ファイルのキャッシュ管理でzipファイルやSQLiteのルーチンも入り込むようになり、ブクブクとソースファイルが大きくなってしまいました。
また、ListViewをVirtualModeにした関係でイベントで選択されたアイテムのしゅとく方法を変更しました。
また、なぜかエラーが発生するため画像読み込みを非同期処理を同期処理に変更しました。
ListViewのVirtualModeは表示されている部分のみアイテムを描画するため、スクロールするほどデータ件数が多い場合、比較的描画のレスポンスが良くなります。だた、自分の環境ではスクロールするたびに1画面分の描画で体感できるほどの待ちが発生しています。表示する画像ファイルのサイズが大きくサムネイル用の縮小画像作成に時間を取られていると思われます。サムネイル用の縮小画像をキャッシュするようにしたので、一度表示したディレクトリはそこそこのレスポンスで動作するようになりますが、自由自在にディレクトリを移動するような使い方をする場合大分ストレスを感じるアプリケーションになっています。個人的な使い方としては問題は少ないのですが、何か対策を思いついたら試してみたいと思います。
コメント