帳票出力の迷宮を抜け出せ:WPF FlowDocumentとPrintDialogによる、プロフェッショナルな帳票アーキテクチャの構築
業務アプリケーションの開発において、もっとも「地味ながら、設計のセンスが露骨に出る」領域。それが帳票出力だ。
多くの開発者は、画面に見えている `DataGrid` や `ListBox` をそのまま無理やり印刷しようとしたり、あるいは固定レイアウトのPDF生成ライブラリに頼り切り、動的なデータ量(行数の増減)への対応に苦慮し、最終的に「レイアウトが崩れる」「ページ番号が正しく出ない」といったバグの泥沼に沈んでいく。
もし君が、データの増減に合わせて美しくページが切り替わり、プレビューと実印刷が完全に一致する、堅牢な帳票システムを構築したいのであれば、WPFの `FlowDocument` を使いこなす術を学ぶ必要がある。
本稿では、単なるコードの書き方ではなく、「なぜその設計が必要なのか」というアーキテクチャの観点から、VB.NETを用いた高度な帳票実装の極意を伝授する。
1. 陥りがちな「アンチパターン」とその代償
まず、私が現場で目にする「失敗する設計」を指摘しておこう。
❌ アンチパターン:UI要素の直接印刷
画面上のコントロール(`DataGrid` 等)を `VisualBrush` などを使って無理やり画像として印刷する手法だ。
- 致命的な欠陥: 解像度が不足し、文字がボケる。また、ページをまたぐデータの制御が不可能であり、データの増減に全く対応できない。これは「印刷」ではなく、単なる「画面キャプチャ」だ。
❌ アンチパターン:固定座標による描画
`DrawingContext` を使い、座標を `(x, y)` で指定して描画する手法だ。
- 致命的な欠陥: フォントサイズが変わった瞬間にレイアウトが崩壊する。動的なテキスト(住所や備考欄など)が含まれる業務帳票では、保守コストが指数関数的に増大する。
✅ 正解:FlowDocumentによる「流動的」なドキュメント設計
`FlowDocument` は、テキストやテーブルを「文書構造」として定義する。データ量に応じて、エンジンが自動的にページ境界を計算し、改ページを制御する。これこそが、業務帳票における正解である。
2. 高度な帳票システムの設計思想
プロフェッショナルな実装には、「データの生成(Model)」「文書の構築(Document Generator)」「表示と出力(View/Service)」を明確に分離するアーキテクチャが必要だ。
1. Data Model: データベースから取得した純粋なデータオブジェクト。
2. Document Generator: Modelを受け取り、`FlowDocument` オブジェクトを組み立てるロジック。UI(Window)に依存させてはいけない。
3. Print Service: `PrintDialog` を制御し、生成されたドキュメントを物理デバイスへ送る責務を持つ。
この分離により、ユニットテストが可能になり、将来的に「プレビューはWPFで行い、出力はPDFにする」といった仕様変更にも、Generatorを再利用するだけで対応できる。
3. 実装例:動的帳票生成エンジン
以下に、プロダクションレベルで使用できる、堅牢な設計に基づいた実装コードを示す。
Data Model & Generator
''' <summary>
''' 帳票のデータモデル
''' </summary>
Public Class InvoiceItem
Public Property Description As String
Public Property Quantity As Integer
Public Property UnitPrice As Decimal
Public Property Total As Decimal
Get
Return Quantity * UnitPrice
End Get
End Property
End Class
''' <summary>
''' FlowDocumentを生成する専用のロジッククラス
''' </summary>
Public Class ReportGenerator
''' <summary>
''' 請求書ドキュメントを生成する
''' </summary>
Public Function CreateInvoiceDocument(customerName As String, items As List(Of InvoiceItem)) As FlowDocument
Dim doc As New FlowDocument()
' --- ページ設定 (重要: 印刷サイズに合わせる) ---
doc.PageHeight = 1056 ' A4 サイズ相当 (96 DPI)
doc.PageWidth = 816
doc.PagePadding = New Thickness(50)
doc.ColumnWidth = doc.PageWidth - 100
' --- ヘッダー部 ---
Dim header As New Paragraph(New Run("請求書"))
header.FontSize = 28
header.FontWeight = FontWeights.Bold
header.TextAlignment = TextAlignment.Center
doc.Blocks.Add(header)
doc.Blocks.Add(New Paragraph(New Run($"宛先: {customerName} 様")))
doc.Blocks.Add(New BlockUIContainer(New Separator()))
' --- 明細テーブル部 ---
Dim table As New Table()
table.Columns.Add(New TableColumn() With {.Width = New GridLength(3, GridUnitType.Star)}) ' 説明
table.Columns.Add(New TableColumn() With {.Width = New GridLength(1, GridUnitType.Star)}) ' 数量
table.Columns.Add(New TableColumn() With {.Width = New GridLength(1, GridUnitType.Star)}) ' 単価
table.Columns.Add(New TableColumn() With {.Width = New GridLength(1, GridUnitType.Star)}) ' 合計
' ヘッダー行
Dim headerRow As New TableRow()
headerRow.Background = Brushes.LightGray
headerRow.Cells.Add(CreateTableCell("品名"))
headerRow.Cells.Add(CreateTableCell("数量"))
headerRow.Cells.Add(CreateTableCell("単価"))
headerRow.Cells.Add(CreateTableCell("金額"))
table.Rows.Add(headerRow)
' データ行の追加
For Each item In items
Dim row As New TableRow()
row.Cells.Add(CreateTableCell(item.Description))
row.Cells.Add(CreateTableCell(item.Quantity.ToString()))
row.Cells.Add(CreateTableCell(item.UnitPrice.ToString("C")))
row.Cells.Add(CreateTableCell(item.Total.ToString("C")))
table.Rows.Add(row)
Next
doc.Blocks.Add(table)
' --- フッター部 (合計金額など) ---
Dim totalSum As Decimal = items.Sum(Function(x) x.Total)
doc.Blocks.Add(New Paragraph(New Run($"合計金額: {totalSum:C}")) With {
.FontSize = 18,
.FontWeight = FontWeights.Bold,
.TextAlignment = TextAlignment.Right
})
Return doc
End Function
' ヘルパーメソッド: セル作成の共通化
Private Function CreateTableCell(text As String) As TableCell
Return New TableCell(New Paragraph(New Run(text)))
End Function
End Class
Print Service & UI Integration
''' <summary>
''' 印刷制御を担当するサービス
''' </summary>
Public Class PrintService
''' <summary>
''' FlowDocumentをプリンターへ出力する
''' </summary>
Public Sub Print(doc As FlowDocument)
Dim printDialog As New PrintDialog()
' プリンター選択ダイアログを表示
If printDialog.ShowDialog() = True Then
' 印刷実行
' 注意: FlowDocumentはPrintDialogのPrintDocumentメソッドに直接渡せる
printDialog.PrintDocument(doc, "業務帳票出力")
End If
End Sub
End Class
' --- MainWindow.xaml.vb での利用イメージ ---
Public Class MainWindow
Private Sub btnPreviewAndPrint_Click(sender As Object, e As RoutedEventArgs e) Handles btnPreviewAndPrint.Click
' 1. データの準備 (本来はDBから取得)
Dim dummyData As New List(Of InvoiceItem) From {
New InvoiceItem With {.Description = "ソフトウェア開発費", .Quantity = 1, .UnitPrice = 500000},
New InvoiceItem With {.Description = "保守運用費 (1ヶ月)", .Quantity = 12, .UnitPrice = 50000},
New InvoiceItem With {.Description = "追加コンサルティング", .Quantity = 5, .UnitPrice = 20000}
}
' 2. Generatorによるドキュメント生成
Dim generator As New ReportGenerator()
Dim myDoc As FlowDocument = generator.CreateInvoiceDocument("株式会社サンプル様", dummyData)
' 3. プレビュー表示 (FlowDocumentReaderを使用)
' FlowDocumentReaderは、FlowDocumentを閲覧するための専用コントロール
Dim reader As New FlowDocumentReader(myDoc)
reader.Owner = Me
reader.ShowDialog()
' 4. 印刷実行
Dim printer As New PrintService()
printer.Print(myDoc)
End Sub
End Class
4. 伝説のアーキテクトが教える「極限の注意点」
コードを書いただけでは、プロフェッショナルとは呼べない。実務で必ず直面する「落とし穴」を回避するための知見を授ける。
① DPIとスケーリングの罠
WPFの内部単位は `1/96インチ` である。しかし、最近の高精細なプリンターや、Windowsの「拡大縮小設定(DPIスケーリング)」が150%になっている環境では、プレビューと印刷結果がズレることがある。
`FlowDocument` を作成する際は、必ず `PageHeight` と `PageWidth` を明示的に指定し、ハードウェアの解像度に依存しない設計を心がけよ。
② メモリリークとオブジェクトのライフサイクル
大量のデータを扱う帳票の場合、`FlowDocument` は非常に多くのメモリを消費する。
- 印刷が終わったら、ドキュメントオブジェクトへの参照を速やかに解放すること。
- 特に、`FlowDocumentReader` を使用する場合、その `Owner` プロパティを適切に管理し、ウィンドウが閉じられた際にリソースが解放されるように設計せよ。
③ データベース連携時のタイミング
データベースからデータを取得する際、`FlowDocument` の生成中に接続を維持し続けるのは避けるべきだ。
「データ取得 → 閉じる → ドキュメント生成」 の順序を徹底せよ。生成プロセス中にDB接続がタイムアウトしたり、デッドロックを引き起こしたりするのは、初歩的なミスだ。
④ プレビューと印刷の「完全一致」
`FlowDocumentReader` で表示される内容と、`PrintDialog` で出力される内容は、基本的には同一だが、マージンの解釈が異なる場合がある。
設計段階で、「画面で見せるためのUI」と「紙に刷るためのドキュメント」を、同じ `FlowDocument` オブジェクトから派生させること。これが、プレビューと実物の乖離を防ぐ唯一の手段である。
結論
WPFにおける帳票実装は、単なる「見た目の作成」ではない。それは、「データの構造を、いかにして物理的な媒体(紙やPDF)へ正確に写像するか」という、極めて論理的な変換プロセスである。
`FlowDocument` を中心に据え、生成ロジックを分離したアーキテクチャを採用することで、君のアプリケーションは、どんなに複雑なビジネス要求にも耐えうる、堅牢で美しいものへと進化するだろう。
さあ、コードを書き直せ。妥協したレイアウトは、ユーザーの信頼を失う。
