GDI+の深淵:PrintPageEventArgsを支配し、帳票印刷の「限界」を超える
多くのVB.NET開発者が、`PrintDocument`の標準機能に限界を感じ、泥沼のカスタマイズに足を踏み入れ、そしてメモリリークという名の墓場に沈んでいく。
帳票システムは単なる描画ではない。それは「メモリ上に物理的な紙の空間をシミュレートする」という、極めて低レイヤーな作業だ。今回は、`PrintPageEventArgs`を完全に制御し、業務系システムで必須となる「動的なヘッダー・フッター」「透かし」「改ページ制御」を、リソース効率を最大化しながら実装する極意を伝授する。
—
1. PrintPageイベントの「真実」とメモリ管理の鉄則
`PrintPageEventArgs`の`Graphics`オブジェクトは、OSが提供するデバイスコンテキスト(DC)の薄いラッパーに過ぎない。このオブジェクトを安易に使い捨てたり、Disposeを怠ったりすると、WindowsのGDIハンドル制限に抵触し、システムをクラッシュさせる。
鉄則:描画オブジェクトは「使い回す」な、「生成・破棄」のサイクルを制御せよ
`Pen`や`Brush`をクラスメンバで保持し続けるのはアンチパターンだ。描画のたびに生成し、`Using`ステートメントで確実に破棄する。これがメモリ管理の基本である。
‘ 悪い例: メンバ変数として保持し続ける(GDIハンドルリークの温床)
‘ Private _redPen As New Pen(Color.Red)
‘ 良い例: Usingブロックでスコープを限定する
Private Sub PrintDocument_PrintPage(sender As Object, e As PrintPageEventArgs) Handles pd.PrintPage
‘ グラフィックスの品質を設定(アンチエイリアスは重い。帳票ならNoneで十分)
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.None
Using p As New Pen(Color.Black, 1.0F)
‘ 描画処理
e.Graphics.DrawRectangle(p, e.MarginBounds)
End Using
End Sub
—
2. 「社外秘透かし」の動的描画と描画順序の設計
透かしはページ描画の最後に行うべきではない。コンテンツの下に配置するため、`DrawString`を座標変換と併用して描画する。ここで重要なのは、`Graphics.TranslateTransform`と`RotateTransform`を活用し、描画の原点を論理的に管理することだ。
Private Sub DrawWatermark(g As Graphics, bounds As Rectangle)
Dim watermark As String = “社外秘・複製厳禁”
Using font As New Font(“Arial”, 60, FontStyle.Bold)
Using brush As New SolidBrush(Color.FromArgb(50, Color.Gray)) ‘ アルファ値で透過制御
g.Save() ‘ 現在の座標系を保存
g.TranslateTransform(bounds.Width / 2, bounds.Height / 2)
g.RotateTransform(-45)
Dim size As SizeF = g.MeasureString(watermark, font)
g.DrawString(watermark, font, brush, -size.Width / 2, -size.Height / 2)
g.Restore() ‘ 元の座標系へ復帰
End Using
End Using
End Sub
—
3. 改ページ制御(HasMorePages)の極限ロジック
帳票における最大の課題は、「明細行がどこで切れるか」の動的判定である。これを制御するために、静的変数やクラスレベルのインデックスを駆使し、状態を維持する。
Private _currentRowIndex As Integer = 0 ‘ 現在の行位置を永続化
Private Sub PrintDocument_PrintPage(sender As Object, e As PrintPageEventArgs) Handles pd.PrintPage
Dim yPos As Single = e.MarginBounds.Top
Dim rowHeight As Single = 20.0F
While _currentRowIndex < _dataList.Count
' 改ページ判定: 次の行がマージンを超えるか?
If yPos + rowHeight > e.MarginBounds.Bottom Then
e.HasMorePages = True ‘ 次のページへ
Return
End If
‘ 描画処理
e.Graphics.DrawString(_dataList(_currentRowIndex), _font, Brushes.Black, e.MarginBounds.Left, yPos)
yPos += rowHeight
_currentRowIndex += 1
End While
e.HasMorePages = False ‘ 全終了
_currentRowIndex = 0 ‘ インデックスをリセット(再印刷対応)
End Sub
—
4. 伝説的アーキテクトからの提言:印刷プレビューの罠
`PrintPreviewDialog`は便利だが、高解像度のプリンタドライバとプレビュー画面のDPI不一致が原因で、「プレビューではズレるが印刷すると綺麗」という現象が頻発する。
これを解消するには、`PrintPageEventArgs.Graphics.DpiX` / `DpiY` を取得し、描画座標をスケーリングする補正係数を導入することだ。
- 極限のTips:
- `e.Graphics.PageUnit = GraphicsUnit.Millimeter` を設定せよ。ピクセル計算でハマる時代は終わった。ミリ単位でレイアウトを計算すれば、解像度依存のバグは消滅する。
- 複雑な表を描画する場合、`GraphicsPath`を使用して外枠を一括生成し、最後に`DrawPath`で描画せよ。個別に線を引くよりも描画負荷を劇的に下げられる。
最後に:なぜVB.NETなのか
VB.NETを「レガシー」と呼ぶ者は、その真のポテンシャルを知らない。`System.Drawing`(GDI+)を極め、Windows APIとの境界線を自在に行き来する能力は、現代のWebフロントエンド開発にはない「ハードウェアに近い手触り」を我々に提供してくれる。
コードは、ただ動けばいいのではない。メモリの呼吸を感じ、OSのリソースを敬い、枯れた技術を芸術の域にまで高めること。 それこそが、我々エンジニアが追求すべき道だ。
現場で困ったら、まずは `Using` ステートメントが正しいか、そして `Graphics` の状態を `Save/Restore` しているかを確認せよ。そこに全ての答えがある。
