358本のフォントを全部読み込むまでに一日が溶けた。失敗。ログの保存先を4つも試しながら、ようやく呼吸が整った。QuestPDFでお気に入りを冊子化した瞬間、肩の力が抜けた。
この文章は、WPF版FontViewerAppをWindows 10上で量産ビルドし、ログとPDFに逃げ場を作るまでの制作エッセイです。焦った。けれど面白かった。
対象読者
- Windowsのシステムフォントを一括管理したいデスクトップ開発者
- QuestPDFや実行時コンパイルの一部をビルド時に済ませるオプションを使った配布を検討している.NETエンジニア
- ログ保存先やお気に入り機能の粘り強い実装例を知りたい人
記事に書いてあること
- ログフォールバックを4段構えにした理由と効果
- お気に入り→PDF出力までを繋いだ方法
- QuestPDFを使って1ページ4フォント/160pt枠におさめたPDF設計
- ビルドスクリプトでx64/x86/arm64を自己完結型に束ねるパイプライン
前提知識
- WPF (XAML + MVVM) の基本操作
- QuestPDFの
Document.Create構文
dotnet publishのランタイム識別子 (RID) と実行時コンパイルの一部をビルド時に済ませるオプション
作ったもの
システムフォントを列挙し、検索、プレビュー、☆登録、PDF出力、そしてx64/x86/arm64向け単一EXEの配布までを一人で完結できるFontViewerAppです。UIはGrid分割で、左にリスト・右にプレビューを置き、プレビュー用テキストは6言語をあらかじめ埋め込んでいます。お気に入りは%LocalAppData%下のJSONに永続化し、PDFにはQuestPDFを採用しました。
358本を拾い上げるまでの戦い
最初はSystem.Drawingへの依存でこけました。System.Drawing.Commonがself-containedに含まれず、実行時に「ファイルが見つかりません」。焦った。
最終的にはWPF Fonts.SystemFontFamiliesを主軸にし、PrivateFontCollectionはファイル名推測のフォールバックとして割り切りました。さらに、ログを吐き出せないと調査が進まないので、%LocalAppData%・実行フォルダ・%TEMP%・カレントディレクトリの順で4回書き込みを試す関数を入れています。
ログファイルの保存先を決定する処理:
private static string GetLogFilePath()
{
if (_logFilePath != null)
return _logFilePath;
lock (_logLock)
{
if (_logFilePath != null)
return _logFilePath;
var candidates = new List<string>();
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var logDir = Path.Combine(appDataPath, "FontViewerApp");
Directory.CreateDirectory(logDir);
candidates.Add(Path.Combine(logDir, "font-debug.log"));
// ... 中略: exeディレクトリ → Temp → カレントディレクトリ ...
foreach (var candidate in candidates)
{
File.AppendAllText(candidate, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ログファイル初期化{Environment.NewLine}", Encoding.UTF8);
_logFilePath = candidate;
return _logFilePath;
}
}
}
ローカルでPowerShellを回してみると、C:\Windows\FontsだけでTTF 326本 + TTC 32本 = 358本。%LocalAppData%\Microsoft\Windows\Fontsは今回は0本でしたが、空でもログに痕跡が残るので後から追いやすくなりました。
お気に入りを信頼できる形にする
お気に入り機能は%LocalAppData%\FontViewerApp\favorites.jsonに書き出し、ViewModel側はお気に入りの切り替え→UI更新で連動させました。お気に入り状態の判定やPDF出力コマンドの更新をサボると中途半端な状態になるので、コマンド更新を徹底しています。意外だ。地味な通知が一番効いた。
お気に入りの保存処理:
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"FontViewerApp");
Directory.CreateDirectory(appDataPath);
_favoritesFilePath = Path.Combine(appDataPath, "favorites.json");
_favoriteFontFamilies = new HashSet<string>();
LoadFavorites();
public void ToggleFavorite(string fontFamily)
{
if (IsFavorite(fontFamily))
RemoveFavorite(fontFamily);
else
AddFavorite(fontFamily);
}
PDF出力の処理:
private void ExportFavoritesToPdf()
{
if (FavoriteFonts.Count == 0)
return;
var dialog = new SaveFileDialog
{
FileName = $"お気に入りフォント一覧_{DateTime.Now:yyyyMMdd_HHmmss}.pdf",
DefaultExt = "pdf"
};
if (dialog.ShowDialog() == true)
{
var fonts = FavoriteFonts.ToList();
if (_pdfExportService.ExportFavoritesToPdf(fonts, PreviewText, dialog.FileName))
{
MessageBox.Show($"PDFファイルを出力しました。\n{dialog.FileName}",
"成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
多言語プレビューを崩さないUI
プレビュー用の文章は英語・フランス語・ヒンディー語・中国語・日本語・記号列の6行をデフォルト持ちしています。フォントサイズを8〜200ptの範囲で制御するPreviewFontSizeを設けて、行間を自動調整。これならConsolasのような等幅もNoto Sans JPも破綻せずに表示できます。
プレビューテキストの初期値:
private string _previewText =
"English: The quick brown fox jumps over the lazy dog\n" +
"Français: Le renard brun rapide saute par-dessus le chien paresseux\n" +
"हिन्दी: तेज़ भूरी लोमड़ी आलसी कुत्ते के ऊपर कूदती है\n" +
"中文: 敏捷的棕色狐狸跳过懒惰的狗\n" +
"日本語: 素早い茶色の狐が怠け者の犬を飛び越える\n" +
"0123456789 !@#$%^&*()";
4枠レイアウトでPDFを束ねる理由
QuestPDFでは1ページあたり4フォント、各160pt枠と8ptスペーサを固定化しました。フォントプレビューの中でファイル名・タイプ・プレビュー文を塊にしておけば、A4で見切れない。FontViewerApp-x64.exe単体配布でもPDFを開けば全ての情報が乗っているので、アプリを起動しないと分からない問題を避けられます。
PDF生成の処理:
var document = Document.Create(container =>
{
var fontGroups = fonts
.Select((font, index) => new { font, index })
.GroupBy(x => x.index / 4)
.Select(g => g.Select(x => x.font).ToList())
.ToList();
foreach (var fontGroup in fontGroups)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1.2f, Unit.Centimetre);
page.Header().Height(50, Unit.Point).Column(column => { ... });
var fontHeight = 160f;
page.Content().Column(column =>
{
foreach (var font in fontGroup)
{
column.Item().Height(fontHeight, Unit.Point)
.Element(container => { DrawFontPreview(container, font, previewText, fontHeight); });
}
});
});
}
});
ビルドスクリプトで3体系をまとめて吐き出す
dotnet publishを3回叩き、単一ファイルオプションとネイティブライブラリの自己展開オプションを付けたうえで、LatoFontやデバッグ用DLLを削除、最後にFontViewerApp-*.exeをdist直下へCopy-Itemで複製。実行時コンパイルの一部をビルド時に済ませるオプションのおかげで起動は平均1.7秒→0.8秒に短縮できました。重い。でも速い。実測サイズはx64が162MB、x86が152MB、arm64が172MBでした。
最初は、実行時コンパイルの一部をビルド時に済ませるオプションを有効にしていませんでした。でも、起動速度が遅いことに気づき、有効にしてみました。結果、起動時間が約1.7秒から約0.8秒に短縮されました。ファイルサイズは約10MB増えましたが、起動速度の向上は明らかでした。
ビルドスクリプトの一部:
dotnet publish -c Release -r win-x64 --self-contained true `
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true `
-p:PublishReadyToRun=true -p:DebugType=None -p:DebugSymbols=false `
-o "$distDir\\win-x64"
Remove-Item "$distDir\\win-x64\\LatoFont" -Recurse -Force
Copy-Item "$distDir\\win-x64\\FontViewerApp.exe" -Destination "$distDir\\FontViewerApp-x64.exe" -Force
遊び心の余白
お気に入りボタンは最終的に星アイコンに落ち着きました。気持ちが上向いた時に押してもらいたかったからです。クリックで星がキラリと変化するだけですが、長時間のフォント整理で張り詰めた空気を少し柔らかくできます。
GetAllFontsの工程を整理しておく
流れを可視化しておくと、将来SystemFontsの取得方法が増えたときに差し替えやすいです。ログ→ディレクトリ→ファイル情報→WPF補完→ソートという順番を崩さないのがポイント。
実際に試したケース
- デスクトップ (Core i7 / x64): 358フォントを約3.8秒で読み込み、
FontViewerApp-x64.exe起動時ログは%LocalAppData%に確保。PDF出力は4ページ構成で合計1.2MB。
- 古いノートPC (Atom / x86): 実行時コンパイルの一部をビルド時に済ませるオプションの効果で初回起動1.7秒→1.0秒。Favoritesを5件登録してもUIはブロックせず、
favorites.jsonは6KBで収まった。
- Surface Pro X (arm64): ARM64版は172MBだが、
Fonts.SystemFontFamilies補完が必須な環境だった。JSON + PDF出力ともに30秒以上の連続操作でも例外なし。
使ってみて
実際に触りたくなったら、デモを全画面で開いてください。
ポイントは以下の通りです。
- 検索とプレビューは非同期化していませんが、データ変更を自動で通知するコレクションで十分追従します。
- PDF化する前に、お気に入りを1件でも登録しておくとコマンド群が有効化されます。
- x64ビルドは162MBあります。PowerShellで
Unblock-Fileしてから配布すると警告が減りました。
まとめ
- ログパスを4段階にしておけば、Self-Containedアプリでも調査を止めずに済む。
- お気に入り→PDF→配布までをViewModel一つでまとめると、手作業なしで再現可能になる。
- QuestPDFの160pt固定枠と実行時コンパイルの一部をビルド時に済ませるオプションの組み合わせで、重さと起動時間のバランスを取れた。
- ビルドスクリプトでx64/x86/arm64を同じ習慣で出力し、dist直下を常に最新に保つ。
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。誰かのデスクトップにも、静かなフォント棚が整いますように。