【実務・中級編】【上級者向け】WPFのFlowDocumentとPrintDialogを駆使したVB.NETでの高度な帳票プレビュー・印刷機能の実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

帳票出力の迷宮を抜け出せ: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` を中心に据え、生成ロジックを分離したアーキテクチャを採用することで、君のアプリケーションは、どんなに複雑なビジネス要求にも耐えうる、堅牢で美しいものへと進化するだろう。

さあ、コードを書き直せ。妥協したレイアウトは、ユーザーの信頼を失う。

タイトルとURLをコピーしました