ℹ️
INFO
この記事では、.NET 10のGeneric HostとWPFを組み合わせた画像処理パイプラインの実装を解説します。
最初に掘ったのは Docs/first_session_guide.html のヒントだった。✨ 三層を分けて学べと言われた瞬間、Core・Components・DesktopUIのソリューション構造がただの雛形ではないと腹を括った。
⚠️
WARNING
Generic Hostの登録を読み飛ばしてパイプラインを動かしたら、WPF側のログは沈黙したままだった。焦った。
そこでアプリの起動処理とパイプライン実行の両端を往復しながら、.NET 10での依存関係の通し方をノートに落とした記事がこれだ。
対象読者
- マルチプロジェクト構成のWPFアプリを分解しながら .NET の管理方法を掴みたい人
- Generic Host / IOptions / MVVM の接点を体験し直したいC#エンジニア
- 画像バッチ処理パイプラインを小さく試し、進捗UIまで持っていきたい制作者
記事に書いてあること
- パイプライン実行処理が画像の読み込み→フィルタ処理→出力をどう束ねるかと、キュー(32枠)の詰まり具合の見える化
- 画像リサイズ処理の本音(600px・品質90)と、高品質な設定を崩さないためのシミュレータ
- アプリ起動時の依存関係登録と、現在の構成を可視化するタブの読み方
- 進捗表示を実装してUIを温かく保つためのコマンド整理とログ設計
- 初学者向けガイドで提示されている学習チェックリストを実際のコードに照らして消化する方法
入り口を変えた理由
このソリューションは ImagePipeline.sln にWPFプロジェクトしか登録していない。CoreとComponentsは参照されるだけで、Solution Explorerには顔を出さない。
意外だ。
プロジェクト管理の練習用だから、依存方向を目で追わせたかったのだと理解した。
だから記事でもまず「構造を追う動作」から書く。プロジェクトファイルの参照設定が真の接続図になるし、そこからアプリ起動時の依存関係登録へ潜るのが安全ルートだった。
詳しい初学者向けの手順は、HTML版のガイドを用意してある(first_session_guide.html をダウンロード)。ソリューションを開く前にざっと目を通しておけば、この記事と往復しやすい。
ビルド済みの実行ファイルも用意してある(dotnetProjectForWindows_10-x64.exe をダウンロード)。Visual Studioがなくても動作確認できる。
パイプラインを分解したノート
パイプライン実行処理は3つの依存を受け取っただけで、UIのことを一切知らない。キューを32枠にし、ファイル列挙と処理を別スレッドで分けた最小実装だ。
以下が骨格で、特にキュー作成と読み取りのペアが要だった。
// DesktopUI/Services/PipelineRunner.cs(抜粋)
var channel = Channel.CreateBounded<string>(new BoundedChannelOptions(32)
{
FullMode = BoundedChannelFullMode.Wait
});
var producer = Task.Run(async () =>
{
await foreach (var path in _source.EnumerateFilePathsAsync(inputFolder, cancellationToken))
{
await channel.Writer.WriteAsync(path, cancellationToken);
}
channel.Writer.TryComplete();
}, cancellationToken);
var worker = Task.Run(async () =>
{
await foreach (var path in channel.Reader.ReadAllAsync(cancellationToken))
{
var bytes = await File.ReadAllBytesAsync(path, cancellationToken);
foreach (var filter in _filters)
{
bytes = await filter.ApplyAsync(bytes, cancellationToken);
}
await _exporter.ExportAsync(outputFolder, path, bytes, cancellationToken);
progress.ReportProcessed(++processed);
}
}, cancellationToken);
このままだと「順次処理のイメージ」が抽象的なので、ソース・フィルタ・エクスポータを順にログへ書き出すミニデモを作った。💡 demo1 で通過→リサイズ→出力の順を体験してほしい。
キュー32枠の重み
重要なポイント
キューの容量を32にした理由が最初は分からなかった。フォルダ走査処理は最大5種類の拡張子を走査するので、入力フォルダが巨大だと列挙処理が暴走する恐れがある。
そこで容量を変えながらワーカー数を増やす体験ツールを demo2 にした。
容量を16に絞るとすぐに「キュー満杯」とログが出る。待機する挙動が腹落ちする。
画像リサイズ処理を信頼するまで
リサイズ処理はGDI+を使った典型だが、ターゲット幅が 600 に固定されている。📌 Docs/first_session_guide は「UIから幅を変える練習を次のステップに」と書いているので、今は固定でもいいが動きを可視化しておきたかった。
実際のコードはこうだ。
// Components/ResizeFilter.cs(抜粋)
if (src.Width <= _options.TargetWidth)
{
return inputBytes;
}
var ratio = (double)_options.TargetWidth / src.Width;
var newWidth = _options.TargetWidth;
var newHeight = (int)Math.Round(src.Height * ratio);
using var dst = new Bitmap(newWidth, newHeight);
using (var g = Graphics.FromImage(dst))
{
g.SmoothingMode = SmoothingMode.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.DrawImage(src, 0, 0, newWidth, newHeight);
}
📝
NOTE
demo3 では元画像4032×3024を読み込んだと仮定し、600pxへ縮小するとピクセル数が12.2MP→0.27MPに落ちることをリアルタイムで確認できる。
品質90のJPEGエンコーダに固定されている点も併記した。
依存関係の配線とアーキテクチャタブの読み方
アプリ起動処理がこのプロジェクトの心臓だ。⚡ Generic Host で画像ソース、フィルタ、出力処理を登録し、ウィンドウのデータコンテキストも依存関係注入で設定している。
全部を一枚絵にしたのが demo4 で、どの契約がどの実装を受け取っているかをクリックで追える。
// DesktopUI/App.xaml.cs(抜粋)
services.AddSingleton<IImageSourceProvider, FolderSource>();
services.AddSingleton<IImageFilter, NoOpFilter>();
services.Configure<ResizeOptions>(o =>
{
o.TargetWidth = 600;
o.JpegQuality = 90;
});
services.AddSingleton<IImageFilter, ResizeFilter>();
services.AddSingleton<IImageExporter, FileExporter>();
services.AddSingleton<IAppInfoService, AppInfoService>();
services.AddSingleton<Services.PipelineRunner>();
services.AddSingleton<MainViewModel>();
アーキテクチャタブは、この登録結果を要約して表示している。
どの構成でビルドされたかを即座に確認できるよう demo6 でフィルタのオン/オフを切り替えられるビューも用意した。
進捗とUIの握り方
進捗表示の実装
進捗表示処理は、処理完了が呼ばれるたびにログに「進捗: X/Y」を追加している。最初は総数がゼロのままで百分率が出ず、ProgressBarが止まった。
そこで開始報告をパイプライン実行の冒頭に移したら即座に解決した。✅ demo5 で開始→処理完了→エラー→完了の順を体験できる。
短文ログをUIに流し、ユーザーが「動いている」と感じられるリズムを意識した。
ランタイム全体像とチェックリスト
✅
SUCCESS
demo7 では読み込み → フィルタ → 出力の3フェーズを何度でも往復できる。
demo8 には Docs/first_session_guide の「JS経験者向けチェックリスト」を落とし込んだ。🎯 5項目中3つチェックできれば、midori_new011 のような大型アプリ(InteractiveStoryTellerApp)へ進む準備になると感じている。
使ってみて
1. demo1-pipeline-sampler.html を全画面で開き、読み込み→フィルタ→出力のログが実際のC#コードと一致するか確かめる。
2. Visual StudioでデスクトップUIプロジェクトをスタートアップに設定し、開始コマンドを実行してから demo5 のシミュレータと同じ手順で進捗ログが流れるか比較する。
3. Docs/first_session_guide.html の「次の一歩」を読み、demo8 のチェックボックスをすべて埋められたらリサイズ設定をUIから切り替える課題に挑戦する。
4. 旧記事 midori_new010 のWPFノウハウと照らし合わせ、Generic Host を使っていない頃との違いを感じてほしい。
まとめ
主要なポイント
- マルチプロジェクト構成でもアプリ起動時の依存関係登録を軸にすれば全体像を追いやすく、構成情報サービスがその配線結果をUIで見せている
- パイプライン実行処理はキュー(容量32)でフォルダ列挙とフィルタ処理を分離し、進捗報告を通じてUIへ進捗とエラーを届けている
- 画像リサイズ処理は幅600px / 品質90を既定にし、高品質な補間方法で画像を再サンプリングしてからJPEGエンコーダで書き出す
- 開始・停止・構成情報更新のコマンドが MVVMの芯となり、ログをコレクションで束ねてUIの温度を保っている
- Docs/first_session_guide のチェックリストを demo8 に落とし込むことで、学習の観察ポイントを明文化できた
さらに深く学ぶには
最後まで読んでくださりありがとうございます。🎉 CoreとComponentsを交互に開き続けた夕方の静けさを、この記事とデモに閉じ込めました。同じように三層構造と向き合う方の助けになれば嬉しいです。