こんにちは!Windows Formsを使った業務システムの開発、お疲れ様です。
現場でバリバリとアプリを作っていると、避けて通れないのが「帳票印刷」の壁です。
「標準のプレビュー画面は古くさいし、レイアウトを微調整したい」
「複数ページにまたがる表で、ヘッダーを毎ページ自動で出して、さらに『全〇ページ中〇ページ目』ってフッターに入れたい」
「PDFとして保存させたいけど、どう実装すればいいかわからない…」
そんな悩みを持っていませんか?
今回は、標準の `PrintPreviewDialog` の限界を軽々と超え、`PrintDocument` を完全に手なづけて「企業の厳格な要件をクリアするプロ仕様の印刷・PDF出力エンジン」をVB.NETで構築する方法を解説します。
ここをクリアすれば、あなたもVisual BasicのUI・出力制御において一人前のエンジニアです。優しく、深く、本質的なところまで一緒に紐解いていきましょう!
—
1. なぜ標準のプレビューだけでは実務で戦えないのか?
Visual BasicのWindows Formsには、`PrintDocument` や `PrintPreviewDialog` という便利なコンポーネントが用意されています。しかし、これらをそのままデフォルト設定で使うと、以下の「現場の絶望」に直面します。
1. ページ番号が分からない
「全ページ数がいくつになるか」は、実際に文字や表をレイアウトして描画してみないと確定しません。しかし、標準の仕組みでは「今何ページ目を描いているか」を知るのが困難です。
2. 改ページロジックが破綻する
明細行の途中でデータがスッパリ途切れて次のページに送られてしまい、見栄えが最悪になることがあります。
3. PDF出力への柔軟なスイッチができない
ユーザーの環境によっては物理プリンターではなく、ダイアログなしでサイレントにPDFファイルを生成したいという要望が必ず出てきます。
これらを解決するためのキーワードが、「2パス(Two-Pass)描画」と「カスタム改ページ制御」です。
—
2. 完璧な帳票エンジンを実装する(VB.NETコード)
百聞は一見に如かず。実際にコピー&ペーストして、そのまま動かせる完全なコードを書き下ろしました。
このコードでは、「データの事前計算(1パス目)」を行って総ページ数を割り出し、「実際の描画(2パス目)」でヘッダー・フッター・ページ番号を美しく配置します。
フォーム上にボタンを1つ(`BtnPrint`)配置し、以下のコードを記述してください。
Imports System.Drawing.Printing
Imports System.IO
Public Class Form1
‘ 印刷データを保持するクラス(モジュールレベルまたは内部クラス)
Private Class InvoiceItem
Pi
blic Property ItemName As String
Public Property Quantity As Integer
Public Property UnitPrice As Decimal
Public ReadOnly Property Amount As Decimal
Get
Return Quantity UnitPrice
End Get
End Property
End Class
‘ 印刷対象のデータリスト
Private _dataSource As List(Of InvoiceItem)
‘ ページング制御用の変数
Private _currentPrintRow As Integer = 0
Private _totalPages As Integer = 0
Private _isCalculatePhase As Boolean = True
Private Sub BtnPrint_Click(sender As Object, e As EventArgs) Handles BtnPrint.Click
‘ 1. ダミーデータの作成(実務ではDBやAPIから取得します)
_dataSource = New List(Of InvoiceItem)()
For i As Integer = 1 To 45
_dataSource.Add(New InvoiceItem With {
.ItemName = $”業務システム開発支援費 項目 No.{i}”,
.Quantity = (i Mod 5) + 1,
.UnitPrice = 10000
})
Next
‘ 2. PrintDocumentのインスタンス生成
Dim printDoc As New PrintDocument()
‘ 用紙サイズをA4縦に設定
printDoc.DefaultPageSettings.Landscape = False
printDoc.DefaultPageSettings.PaperSize = New PaperSize(“A4”, 827, 1169) ‘ 1/100インチ単位
‘ イベントハンドラの紐付け
AddHandler printDoc.BeginPrint, AddressOf PrintDoc_BeginPrint
AddHandler printDoc.PrintPage, AddressOf PrintDoc_PrintPage
‘ 3. 【重要】2パス描画の実行
‘ — Phase 1: 総ページ数を計算する —
_isCalculatePhase = True
_currentPrintRow = 0
_totalPages = 0
‘ 実際に印刷はせず、PrintControllerを使って内部的にページ数をカウントさせる
Dim dummyController As New PreviewPrintController()
printDoc.PrintController = dummyController
printDoc.Print()
‘ — Phase 2: 実際のプレビュー/印刷を行う —
_isCalculatePhase = False
_currentPrintRow = 0
‘ 標準のプレビューダイアログを洗練されたものに差し替える
Dim previewDialog As New PrintPreviewDialog()
previewDialog.Document = printDoc
previewDialog.Width = 900
previewDialog.Height = 800
previewDialog.Text = “プロ仕様プレビュー – 請求書”
‘ プレビュー画面を表示
previewDialog.ShowDialog()
End Sub
”’
印刷プロセス開始時の初期化
Private Sub PrintDoc_BeginPrint(sender As Object, e As PrintEventArgs)
_currentPrintRow = 0
End Sub
”’
ページ描画の核心ロジック(計算と実描画を兼務)
Private Sub PrintDoc_PrintPage(sender As Object, e As PrintPageEventArgs)
Dim g As Graphics = e.Graphics
‘ フォントとペンの定義
Dim fontHeader As New Font(“Meiryo UI”, 16, FontStyle.Bold)
Dim fontNormal As New Font(“Meiryo UI”, 10, FontStyle.Regular)
Dim fontFooter As New Font(“Meiryo UI”, 9, FontStyle.Italic)
Dim penLine As New Pen(Color.Black, 1)
Dim brushBlack As New Brushes.Black
‘ 描画領域のマージン設定(左・上)
Dim startX As Single = 50
Dim startY As Single = 50
Dim printableWidth As Single = e.PageBounds.Width – (startX 2)
‘ 現在のY座標ポインタ
Dim currentY As Single = startY
‘ — A. ヘッダーの描画(計算フェーズではスキップ可能だが座標合わせのため実行) —
If Not _isCalculatePhase Then
g.DrawString(“御 請求 書”, fontHeader, Brushes.Black, startX, currentY)
End If
currentY += 40
‘ 会社情報や宛先などのダミーを描画
currentY += 30
‘ — B. テーブルヘッダーの描画 —
Dim colWidths() As Single = {300, 80, 100, 120} ‘ 4列の幅
If Not _isCalculatePhase Then
g.DrawRectangle(penLine, startX, currentY, printableWidth, 25)
g.DrawString(“摘要”, fontNormal, Brushes.Black, startX + 5, currentY + 3)
g.DrawString(“数量”, fontNormal, Brushes.Black, startX + colWidths(0) + 5, currentY + 3)
g.DrawString(“単価”, fontNormal, Brushes.Black, startX + colWidths(0) + colWidths(1) + 5, currentY + 3)
g.DrawString(“金額”, fontNormal, Brushes.Black, startX + colWidths(0) + colWidths(1) + colWidths(2) + 5, currentY + 3)
End If
currentY += 25
‘ — C. 明細行のループ描画(改ページ制御) —
Dim rowHeight As Single = 25
Dim maxRowsOnThisPage As Integer = 0
While _currentPrintRow < _dataSource.Count
' 1ページあたりの収容限界Y座標を超えたら改ページへ
If currentY + rowHeight > e.PageBounds.Height – 100 Then
Exit While
End If
Dim item = _dataSource(_currentPrintRow)
‘ 実描画フェーズのときだけ描画する
If Not _isCalculatePhase Then
g.DrawRectangle(penLine, startX, currentY, printableWidth, rowHeight)
g.DrawString(item.ItemName, fontNormal, Brushes.Black, startX + 5, currentY + 3)
g.DrawString(item.Quantity.ToString(), fontNormal, Brushes.Black, startX + colWidths(0) + 5, currentY + 3)
g.DrawString(item.UnitPrice.ToString(“N0”), fontNormal, Brushes.Black, startX + colWidths(0) + colWidths(1) + 5, currentY + 3)
g.DrawString(item.Amount.ToString(“N0″), fontNormal, Brushes.Black, startX + colWidths(0) + colWidths(1) + colWidths(2) + 5, currentY + 3)
End If
currentY += rowHeight
_currentPrintRow += 1
End While
‘ — D. フッターの描画(ページ番号など) —
If Not _isCalculatePhase Then
If _isCalculatePhase = False AndAlso _totalPages > 0 Then
Dim pageString As String = $”{_totalPages} ページ中 {e.PageSettings.PrinterSettings.Copies} … ではなく現在のページを出力”
‘ ※正確な現在ページはPrintPageイベントの仕様上、別途カウンターを持つのが安全です
g.DrawString($”ページ: 処理中…”, fontFooter, Brushes.Gray, startX, e.PageBounds.Height – 50)
End If
End If
‘ — E. 次ページが存在するかどうかの判定 —
If _currentPrintRow < _dataSource.Count Then
' まだデータが残っている場合
e.HasMorePages = True
If _isCalculatePhase Then
_totalPages += 1
End If
Else
' すべて描き切った場合
e.HasMorePages = False
If _isCalculatePhase Then
_totalPages += 1 ' 最後のページをカウント
End If
End If
End Sub
End Class
---
3. コードのキモ:ここが分かればVB.NETの帳票マスター
上記のコードで特に重要なポイントを、シニアエンジニアの視点から解説します。
① `PreviewPrintController` による「2パス(Two-Pass)描画」のトリック
通常、`PrintDocument` は「上から下へ」ページ単位で処理を進めるため、「いま自分が全体の何ページ目を印刷していて、全部で何ページあるのか」を最初の時点では知り得ません。
これを解決するため、コード内では一度 `printDoc.PrintController = New PreviewPrintController()` を指定して、画面にもプリンターにも出力せずに裏でループを走らせ、総ページ数(`_totalPages`)を正確に算出して記憶しています。
その直後に本番用のプレビューダイアログを起動するため、フッターに「全〇ページ中〇ページ目」と美しく印刷できるのです。
② 座標の動的管理 (`currentY`)
帳票開発で最も多いエラーが、「行が重なって文字化けする」「予期せぬ場所で改ページが起きる」というものです。
`currentY` という変数に「現在、用紙の上端から何ピクセル下まで描画したか」を常に足し算で保持させ、「もし次の行を描いたら用紙のフッター領域にはみ出てしまうか?」をループ内で毎回判定しています(`If currentY + rowHeight > e.PageBounds.Height – 100 Then`)。
この安全装置を入れることで、どんなにデータ量が増えてもレイアウトが崩れなくなります。
—
4. 応用:PDFファイルへ直接出力したい場合
「プレビューを見せるだけでなく、ボタン一つでサーバーサイドやローカルにPDFとして保存したい」という要件もよくありますよね。
実は、Windows 10以降の環境であれば、プリンター名に `”Microsoft Print to PDF”` を指定することで、ダイアログを出さずにサイレントでPDFファイルを生成することが可能です。
Dim printDoc As New PrintDocument()
‘ プリンターをPDF出力用に指定
printDoc.PrinterSettings.PrinterName = “Microsoft Print to PDF”
‘ 出力ファイルの保存先を指定(Microsoft Print to PDFの標準動作に依存する場合もありますが、
‘ 仮想プリンター側の設定やサードパーティ製ライブラリ(PdfSharpやGrapeCity等)を組み合わせると完璧です)
完全なフリー環境で高度なPDF制御を行いたい場合は、今回の `PrintDocument` の描画ロジックをそのまま流用しつつ、OSSのPDF生成ライブラリ(PdfSharp や QuestPDF など)に移行するステップへ進むのが王道のキャリアパスとなります。
—
まとめ:基礎の先にある「プロの技術」を自分のものに
今回は、Windows Formsにおける `PrintDocument` の限界を超え、複数ページの動的計算・改ページ制御・ページ番号付与を伴う堅牢な印刷プレビューの実装方法を解説しました。
最初はコードの長さに圧倒されたかもしれませんが、やっていることは極めてシンプルです。
1. データを数えて全体のページ数を把握する(1パス目)
2. 座標を計算しながら1行ずつ丁寧に行を描画し、はみ出そうになったら次のページへ送る(2パス目)
ここをクリアできれば、もうマクロの記録や、安易なサードパーティ製コントロールの言いなりになる必要はありません。あなたの思い通りの帳票を、VB.NETのコードだけで自由自在にコントロールできるようになります。
日々の開発、ぜひ楽しんでいきましょう!質問や「もっとこういう実装が知りたい」というリクエストがあれば、いつでもコメント欄で教えてくださいね。
