マクロの先へ。WPFのFlowDocumentで実現する「プロ仕様」の帳票印刷アーキテクチャ
こんにちは。現場で数々のシステムを設計・構築してきた、あなたのエンジニア仲間です。
「Excelのマクロは得意だけど、もっと複雑な帳票を、もっと美しく、大量に、正確に印刷するアプリケーションを作りたい……」
もしあなたがそう思っているなら、おめでとうございます。あなたは今、プロフェッショナルなソフトウェア開発者への入り口に立っています。
これまで、VBAなどで「セルに値を書き込んで、そのまま印刷する」という手法をとってきたかもしれません。それは非常に手軽で強力な方法です。しかし、業務システムが成長し、データの量が増え、レイアウトが複雑(例えば、データの量によってページ数が動的に変わる、図解が入る、など)になってくると、Excelベースの印刷では限界が訪れます。
そこで登場するのが、WPF (Windows Presentation Foundation) です。
今日は、WPFの中でも「帳票作成の真髄」とも言える `FlowDocument` と `PrintDialog` を組み合わせた、高度な印刷機能の実装について、その設計思想からコードの書き方まで、魂を込めて解説していきます。
1. 「描画」するのではなく「流し込む」という発想の転換
まず、初心者が最初に陥る罠についてお話ししましょう。
従来のグラフィックライブラリ(GDI+など)を使った印刷は、いわば「キャンバスに絵を描く」作業でした。「座標(x, y)にこの文字を置く」という命令の積み重ねです。これだと、データが一行増えただけで、それ以降の文字がすべて重なってしまう……なんて悲劇が起こります。
一方で、WPFの `FlowDocument` は違います。これは「文書の流れ(Flow)を定義する」設計です。
- 「ここにタイトルを置く」
- 「次に、このデータを表形式で流し込む」
- 「もし入り切らなかったら、自動的に次のページへ送る」
このように、コンテンツの「意味」と「構造」を定義するだけで、WPFが計算して最適なレイアウトを自動生成してくれます。これが、動的な帳票に不可欠な「流動性」の正体です。
2. 実践:動的な帳票プレビューの実装
それでは、実際に「データによってページ数が変わる請求書」のようなイメージで、コードを見ていきましょう。
【XAML】UIの設計
まずは、画面の見た目を作ります。ポイントは、作成した文書を表示するための `FlowDocumentReader` を配置することです。これにより、ユーザーは印刷前に「プレビュー」を確認できます。
<Window x:Class="WpfReportApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="高度な帳票システム" Height="600" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 操作パネル -->
<StackPanel Orientation="Horizontal" Background="#F0F0F0" Padding="10">
<Button x:Name="btnGenerate" Content="帳票生成" Click="btnGenerate_Click" Width="100" Margin="5"/>
<Button x:Name="btnPrint" Content="印刷実行" Click="btnPrint_Click" Width="100" Margin="5"/>
</StackPanel>
<!-- プレビュー表示エリア -->
<FlowDocumentReader x:Name="docReader" Grid.Row="1" />
</Grid>
</Window>
【VB.NET】ロジックの実装
次に、心臓部となるコードです。ここでは「データの構造」を「FlowDocumentのオブジェクト」へと変換するプロセスを丁寧に記述します。
Imports System.Windows.Documents
Imports System.Windows.Media
Class MainWindow
' 帳票のデータを保持する構造体(本来はデータベース等から取得)
Structure ReportItem
Dim Name As String
Dim Price As Integer
End Structure
' 1. 帳票ドキュメントを構築するメソッド
Private Function CreateReportDocument() As FlowDocument
' FlowDocumentのインスタンスを作成
Dim doc As New FlowDocument()
doc.PagePadding = New Thickness(50) ' 余白の設定
doc.ColumnWidth = 500 ' 1列の幅(自動改ページに影響)
' --- タイトル部分 ---
Dim title As New Paragraph(New Run("業務請求書"))
title.FontSize = 28
title.FontWeight = FontWeights.Bold
title.TextAlignment = TextAlignment.Center
doc.Blocks.Add(title)
' --- ヘッダー部分 ---
Dim headerPara As New Paragraph(New Run("発行日: " & DateTime.Now.ToString("yyyy/MM/dd")))
headerPara.TextAlignment = TextAlignment.Right
doc.Blocks.Add(headerPara)
' --- テーブル(明細)部分の構築 ---
Dim table As New Table()
table.BorderThickness = New Thickness(1)
table.BorderBrush = Brushes.Black
' テーブルヘッダーの定義
Dim headerRow As New TableRow()
headerRow.Cells.Add(New TableCell(New Paragraph(New Run("品名")) With {.FontWeight = FontWeights.Bold}))
headerRow.Cells.Add(New TableCell(New Paragraph(New Run("金額")) With {.FontWeight = FontWeights.Bold}))
table.Rows.Add(headerRow)
' --- 動的なデータ流し込み ---
' 本来はDBから取得したリストをループ回します
Dim dummyData As New List(Of ReportItem) From {
New ReportItem With {.Name = "サーバー構築費", .Price = 500000},
New ReportItem With {.Name = "保守運用契約", .Price = 50000},
New ReportItem With {.Name = "ライセンス費用", .Price = 120000},
' 大量のデータがあっても、FlowDocumentが自動でページを分割します
New ReportItem With {.Name = "追加コンサルティング", .Price = 80000}
}
For Each item In dummyData
Dim row As New TableRow()
row.Cells.Add(New TableCell(New Paragraph(New Run(item.Name))))
row.Cells.Add(New TableCell(New Paragraph(New Run(item.Price.ToString("C")))))
table.Rows.Add(row)
Next
doc.Blocks.Add(table)
Return doc
End Function
' ボタンクリック:生成
Private Sub btnGenerate_Click(sender As Object, e As EventArgs)
' ドキュメントを生成してプレビューにセット
docReader.Document = CreateReportDocument()
End Sub
' 2. 印刷実行メソッド
Private Sub btnPrint_Click(sender As Object, e As EventArgs)
If docReader.Document Is Nothing Then
MessageBox.Show("先に帳票を生成してください。")
Return
End If
' PrintDialogのインスタンスを作成
Dim pd As New PrintDialog()
' プリンター選択ダイアログを表示
If pd.ShowDialog() = True Then
' 重要:FlowDocumentをPrintVisualを使用して印刷する
' 引数:印刷対象のVisualオブジェクト, 印刷名
pd.PrintVisual(docReader, "業務帳票の印刷")
End If
End Sub
End Class
3. アーキテクトが教える「陥りやすい罠」と回避策
ここまで読んで「よし、できた!」と思ったあなた。素晴らしい。しかし、プロの現場ではここからが本番です。以下の3つのポイントを意識できるかどうかが、エンジニアとしての格の違いを生みます。
① DPI(解像度)の罠
Windowsのディスプレイ設定で「拡大率 150%」などになっている環境では、画面上の見た目と印刷結果がズレることがあります。WPFは基本的にデバイスに依存しない(DPI-Independent)単位を使用していますが、複雑な画像などを配置する場合は、`VisualTreeHelper` を用いて正確なスケーリングを意識する必要があります。
② メモリ管理とオブジェクトのライフサイクル
`FlowDocument` は、大量のデータを扱うとメモリを消費します。
もし、何千ページもの帳票を生成する場合、一つの `FlowDocument` オブジェクトにすべてを詰め込むのではなく、「ページ単位でドキュメントを分割して処理する」、あるいは「印刷が終わったら確実に参照を外してGC(ガベージコレクション)に任せる」という設計が求められます。
③ レイアウトの「決定論」
`FlowDocument` は自動レイアウトですが、あまりに複雑な入れ子(Tableの中にTableがあるなど)を作ると、レイアウト計算のコストが指数関数的に増大し、プレビューが重くなります。
「構造はシンプルに、データは論理的に」が鉄則です。
まとめ:ここをクリアすれば、VB.NETの基本はバッチリですよ
今回学んだことを整理しましょう。
1. FlowDocumentを使う理由: 「座標」ではなく「構造」でレイアウトを管理し、動的なページ分割を自動化するため。
2. プレビューの重要性: `FlowDocumentReader` を使うことで、ユーザーに安心感を与える。
3. 印刷の仕組み: `PrintDialog.PrintVisual` を使い、Visual要素としてドキュメントをプリンターへ送る。
VBAからの脱却は、単に「書き方が変わる」ことではありません。「データの流れ(Flow)を制御する」という設計思想を手に入れることなのです。
このアーキテクチャをマスターすれば、あなたはもう「Excelの限界」に悩まされることはありません。自信を持って、より高度な業務アプリケーションの構築に挑んでください。応援していますよ!
