C# PrintDocumentの使い方完全ガイド|印刷・プレビュー・用紙設定・エラー対処まで解説
はじめに
C#で帳票、ラベル、レシート、一覧表などを印刷したい場合によく使われるのがPrintDocumentです。PrintDocumentを使うと、Windows Formsアプリケーションからプリンターへ直接印刷したり、印刷プレビューを表示したり、用紙サイズ・向き・余白・ページ分割などを細かく制御できます。
一方で、PrintDocumentは「ボタンを押せば画面がそのまま印刷される」ような部品ではありません。基本的には、印刷したい内容をGraphicsに対して自分で描画する仕組みです。そのため、文字の座標、フォント、罫線、画像、ページ送り、余白などを理解しておかないと、白紙で印刷されたり、プレビューと実際の印刷結果がずれたり、複数ページ印刷がうまくいかなかったりします。
この記事では、C#のPrintDocumentの基本から、印刷プレビュー、プリンター選択、用紙設定、複数ページ印刷、実務でよく使うレイアウト設計、エラー対処までまとめて解説します。Windows Formsで印刷機能を実装したい方、業務アプリで帳票印刷を行いたい方は、ぜひ参考にしてください。
1. C# PrintDocumentとは?できることと全体像
1-1. PrintDocumentの役割とC#での印刷処理の基本
PrintDocumentは、C#で印刷処理を行うための代表的なクラスです。名前のとおり「印刷対象のドキュメント」を表すオブジェクトで、印刷処理の開始、印刷ページの描画、プリンター設定、用紙設定などを扱います。
Microsoftの公式ドキュメントでも、PrintDocumentはWindows Formsアプリケーションからプリンターへ出力を送るための再利用可能なオブジェクトとして説明されています。Microsoft Learn
C#でPrintDocumentを使う印刷処理は、主に次の流れになります。
C#PrintDocument printDocument = new PrintDocument();
printDocument.PrintPage += PrintDocument_PrintPage;
printDocument.Print();
そして、実際に印刷する内容はPrintPageイベントの中で描画します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
e.Graphics.DrawString(
"Hello, PrintDocument!",
new Font("Meiryo", 12),
Brushes.Black,
100,
100
);
}
つまり、PrintDocumentでは「何を印刷するか」をPrintPageイベントで定義し、「いつ印刷するか」をPrint()メソッドで実行するのが基本です。
1-2. PrintDocumentが向いている用途:帳票・ラベル・レシート・一覧表印刷
PrintDocumentは、印刷レイアウトをプログラムで細かく制御したい場合に向いています。
代表的な用途は次のとおりです。
請求書、納品書、見積書などの帳票印刷
商品ラベル、宛名ラベル、バーコードラベルの印刷
POSレジや受付システムのレシート印刷
DataGridViewやリストデータの一覧表印刷
画像やロゴを含む定型フォーマット印刷
ページ番号、ヘッダー、フッター付きの複数ページ印刷
特に、座標を指定して文字や線を描画できるため、「A4用紙のこの位置にタイトルを置く」「明細表の列幅を固定する」「ロゴ画像を左上に配置する」といった細かいレイアウトに対応できます。
一方で、WordやExcelのような高度な文書編集機能をそのまま実現するものではありません。複雑な段組み、リッチテキスト、ページレイアウトの自動調整などを求める場合は、帳票ライブラリやPDF生成ライブラリを検討した方がよい場合もあります。
1-3. PrintDocument・PrintPage・Graphics・PrinterSettings・PageSettingsの関係
PrintDocumentを理解するうえで重要なのが、関連するクラスやイベントの役割です。
PrintDocumentは印刷処理全体を管理します。Print()を呼ぶと印刷が開始され、ページごとにPrintPageイベントが発生します。PrintPageイベントの中では、PrintPageEventArgsを通じてGraphicsオブジェクトを取得し、文字・線・図形・画像などを描画します。
PrinterSettingsは、使用するプリンター、部数、印刷範囲などを扱います。公式ドキュメントでも、PrinterSettingsは文書をどこに、どのように印刷するかを指定する設定として説明されています。Microsoft Learn
PageSettingsは、用紙サイズ、縦向き・横向き、余白などのページ設定を扱います。
関係を整理すると、次のようになります。
PrintDocument
├─ PrintPageイベント
│ └─ PrintPageEventArgs
│ ├─ Graphics:文字・線・画像などを描画する
│ ├─ MarginBounds:余白を除いた印刷領域
│ ├─ PageBounds:ページ全体の領域
│ └─ HasMorePages:次ページがあるかを指定する
├─ PrinterSettings:プリンター名、部数、印刷範囲など
└─ DefaultPageSettings:用紙サイズ、向き、余白など
この関係を押さえておくと、PrintDocumentのコードが読みやすくなります。
1-4. Windows Formsで使う場合の前提と.NET環境別の注意点
PrintDocumentは、主にWindows Formsアプリケーションで使われる印刷機能です。System.Drawing.Printing名前空間に含まれており、Graphicsによる描画を前提としています。
.NET FrameworkのWindows Formsでは比較的そのまま使えますが、.NET 6以降の環境では注意が必要です。System.Drawing.Commonは.NET 6以降、Windows以外のOSではサポート対象外になっており、非Windows環境ではコンパイル警告や実行時例外が発生する可能性があります。Microsoft Learn+1
そのため、PrintDocumentを本格的に使う場合は、基本的にWindowsデスクトップアプリケーションを前提に考えるのが安全です。.NET 6以降のWindows Formsプロジェクトで使う場合は、ターゲットフレームワークをnet6.0-windows、net8.0-windows、net10.0-windowsのようにWindows向けにする必要があります。
例:
XML<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
クロスプラットフォームアプリやWebアプリで印刷・PDF生成をしたい場合は、PrintDocumentではなく、PDF生成ライブラリ、帳票ライブラリ、ブラウザー印刷、サーバーサイド帳票出力などを検討するのが一般的です。
1-5. PrintDocumentで実装できること・できないこと
PrintDocumentで実装できることは多くあります。
実装できること
・文字の印刷
・フォント、文字色、サイズ、太字の指定
・線、四角形、罫線の描画
・画像、ロゴの印刷
・表形式の印刷
・複数ページ印刷
・ページ番号の印刷
・印刷プレビュー
・プリンター選択
・用紙サイズ、向き、余白設定
・ページごとの用紙設定変更
一方で、次のような処理は標準機能だけでは簡単ではありません。
苦手なこと
・HTMLやCSSをそのまま印刷する
・Word文書のような複雑な自動レイアウト
・Excelのシートをそのまま印刷する
・PDFファイルを直接高品質に生成する
・クロスプラットフォームで同一の印刷結果を保証する
・プリンタードライバー差異を完全に吸収する
PrintDocumentは「印刷キャンバスに自分で描画する仕組み」と考えると理解しやすいです。細かい制御ができる反面、レイアウト計算やページ分割は自分で実装する必要があります。
2. C# PrintDocumentの基本的な使い方
2-1. PrintDocumentを使うために必要なusingと準備
PrintDocumentを使うには、主に次の名前空間を使用します。
C#using System;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
Windows Formsアプリケーションで印刷プレビューや印刷ダイアログを使う場合は、System.Windows.Formsも必要です。
.NET 6以降のWindows Formsプロジェクトでは、プロジェクトファイルに次のような設定が必要です。
XML<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
.NET Frameworkの場合は、Windows Formsプロジェクトを作成すれば基本的な参照は最初から含まれていることが多いです。
2-2. PrintDocumentインスタンスの作成方法
PrintDocumentは、通常フォームのフィールドとして保持します。印刷とプレビューで同じPrintDocumentを使うためです。
C#private PrintDocument printDocument = new PrintDocument();
フォームのコンストラクタやLoadイベントで、PrintPageイベントを登録します。
C#public Form1()
{
InitializeComponent();
printDocument.PrintPage += PrintDocument_PrintPage;
}
一度だけイベント登録する場合は、フォームの初期化時に登録するのが一般的です。ボタンを押すたびにイベント登録すると、同じ処理が複数回呼ばれる原因になります。
2-3. PrintPageイベントを登録する
PrintPageイベントは、印刷するページを描画するためのイベントです。PrintDocument.Print()が実行されると、印刷対象のページごとに呼び出されます。
C#printDocument.PrintPage += PrintDocument_PrintPage;
イベントハンドラーは次のように書きます。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
// ここに印刷内容を描画する
}
重要なのは、PrintPageイベント内で描画しなければ印刷結果は白紙になるという点です。画面上のラベルやテキストボックスを配置しただけでは、PrintDocumentには何も印刷されません。
2-4. e.Graphics.DrawStringで文字を印刷する最小サンプル
文字を印刷する最小コードは次のとおりです。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 12);
e.Graphics.DrawString(
"C# PrintDocumentの基本サンプル",
font,
Brushes.Black,
100,
100
);
}
DrawStringの主な引数は次の意味です。
第1引数:印刷する文字列
第2引数:フォント
第3引数:文字の色を表すBrush
第4引数:X座標
第5引数:Y座標
座標は左上を原点として、右方向がX、下方向がYです。印刷では画面と同じように、左上から位置を指定して描画します。
2-5. Printメソッドで印刷を実行する
印刷を実行するにはPrint()メソッドを呼びます。
C#private void buttonPrint_Click(object sender, EventArgs e)
{
printDocument.Print();
}
Print()は印刷ダイアログを表示せず、設定済みのプリンターへ印刷を開始します。公式ドキュメントでも、Print()は印刷ダイアログを使用せずに文書を印刷するメソッドであり、ユーザーに印刷設定を選ばせたい場合はPrintDialogを使うと説明されています。Microsoft Learn
そのため、いきなりPrint()を呼ぶと既定プリンターへ印刷される場合があります。実務アプリでは、印刷前にプレビューや印刷ダイアログを表示する設計にした方が安全です。
2-6. 最初に押さえるべき基本コード全体
以下は、Windows FormsでPrintDocumentを使って文字を印刷する基本コードです。
C#using System;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
public partial class Form1 : Form
{
private readonly PrintDocument printDocument = new PrintDocument();
public Form1()
{
InitializeComponent();
printDocument.PrintPage += PrintDocument_PrintPage;
}
private void buttonPrint_Click(object sender, EventArgs e)
{
printDocument.Print();
}
private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 14);
e.Graphics.DrawString(
"C# PrintDocumentの印刷テストです。",
font,
Brushes.Black,
100,
100
);
e.HasMorePages = false;
}
}
最初はこの形を覚えれば十分です。
1. PrintDocumentを作る
2. PrintPageイベントを登録する
3. PrintPage内でGraphicsに描画する
4. Print()で印刷する
この流れがPrintDocumentの基本です。
3. PrintPageイベントの書き方を理解する
3-1. PrintPageイベントが呼ばれるタイミング
PrintPageイベントは、PrintDocument.Print()によって印刷処理が開始された後、各ページを印刷する直前に呼び出されます。公式ドキュメントでも、OnPrintPageはページを印刷する前にPrintPageイベントを発生させるものとして説明されています。Microsoft Learn
1ページだけ印刷する場合は、基本的に1回だけPrintPageが呼ばれます。複数ページ印刷する場合は、e.HasMorePages = true;を設定することで、次のページ用に再度PrintPageが呼ばれます。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
// 1ページ分を描画する
if (まだ印刷するデータがある)
{
e.HasMorePages = true;
}
else
{
e.HasMorePages = false;
}
}
PrintPageは「全ページを一度に描画する場所」ではなく、「現在の1ページ分を描画する場所」です。この考え方が複数ページ印刷では特に重要になります。
3-2. PrintPageEventArgsで取得できる情報
PrintPageイベントの第2引数であるPrintPageEventArgsには、印刷に必要な情報が含まれています。
よく使うプロパティは次のとおりです。
e.Graphics
文字、線、図形、画像などを描画するためのGraphicsオブジェクト
e.MarginBounds
余白を除いた印刷可能領域
e.PageBounds
ページ全体の領域
e.PageSettings
現在ページの用紙設定
e.HasMorePages
次のページがあるかどうか
e.Cancel
印刷をキャンセルするかどうか
特によく使うのは、e.Graphics、e.MarginBounds、e.HasMorePagesです。Graphicsに描画し、MarginBoundsを基準にレイアウトし、次ページがある場合にHasMorePagesをtrueにします。
3-3. Graphicsを使って文字・線・図形・画像を描画する
PrintDocumentでは、Graphicsを使って印刷内容を描画します。画面描画と同じように、印刷先の紙に対して文字や図形を描くイメージです。
文字を描画する例です。
C#e.Graphics.DrawString("タイトル", font, Brushes.Black, x, y);
線を描画する例です。
C#e.Graphics.DrawLine(Pens.Black, 100, 150, 500, 150);
四角形を描画する例です。
C#e.Graphics.DrawRectangle(Pens.Black, 100, 200, 300, 100);
画像を描画する例です。
C#using Image logo = Image.FromFile("logo.png");
e.Graphics.DrawImage(logo, 100, 100, 120, 60);
Graphicsに対してどのように描画するかが、印刷結果を決めます。フォーム上のコントロールをそのまま印刷するのではなく、印刷用の描画処理を自分で書く点に注意しましょう。
3-4. 座標・単位・印刷位置の考え方
Graphicsで描画する場合、基本的には左上が原点です。
X座標:右へ進むほど大きくなる
Y座標:下へ進むほど大きくなる
たとえば、次のコードは左から100、上から100の位置に文字を描画します。
C#e.Graphics.DrawString("印刷位置のテスト", font, Brushes.Black, 100, 100);
印刷位置を安定させたい場合は、固定値を直接書くよりもMarginBoundsを基準にするのがおすすめです。
C#int x = e.MarginBounds.Left;
int y = e.MarginBounds.Top;
e.Graphics.DrawString("余白内の左上に印刷", font, Brushes.Black, x, y);
こうすると、プリンターや用紙設定によって余白が変わった場合でも、印刷可能な範囲を基準にレイアウトできます。
3-5. MarginBoundsとPageBoundsの違い
MarginBoundsとPageBoundsの違いは、PrintDocumentでよく混乱しやすいポイントです。
PageBoundsはページ全体の領域です。用紙全体のサイズを表します。
MarginBoundsは、余白を除いた領域です。通常、実際の本文や表はMarginBoundsの中に描画します。
C#Rectangle page = e.PageBounds;
Rectangle margin = e.MarginBounds;
e.Graphics.DrawRectangle(Pens.Red, page);
e.Graphics.DrawRectangle(Pens.Blue, margin);
上記のように描画すると、ページ全体と余白内の領域の違いを確認できます。ただし、実際のプリンターでは物理的に印刷できない範囲があるため、ページ端ぎりぎりに描画すると切れることがあります。
実務では、次のように考えると安全です。
PageBounds:用紙全体を把握するために使う
MarginBounds:本文・表・画像などを配置する基準に使う
3-6. Font・Brush・Penなどのリソース管理
Font、Pen、Brush、Imageなどの一部オブジェクトはリソースを使用します。自分で生成したものは、使い終わったらDispose()する必要があります。
C#using Font font = new Font("Meiryo", 12);
using Pen pen = new Pen(Color.Black, 1);
e.Graphics.DrawString("リソース管理の例", font, Brushes.Black, 100, 100);
e.Graphics.DrawLine(pen, 100, 130, 500, 130);
Brushes.BlackやPens.Blackのような組み込みの共有オブジェクトは、通常自分でDispose()しません。一方、new Font()、new Pen()、Image.FromFile()などで作成したものはusingで管理するのが安全です。
印刷処理は大量ページになることもあります。リソースを解放しないままページごとに生成し続けると、メモリ使用量が増えたり、GDIリソース不足になったりする可能性があります。
4. 文字・画像・表を印刷する実装例
4-1. テキストを任意の位置に印刷する
任意の位置にテキストを印刷するには、DrawStringでX座標とY座標を指定します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 12);
e.Graphics.DrawString("顧客名:山田 太郎", font, Brushes.Black, 100, 100);
e.Graphics.DrawString("請求日:2026/06/16", font, Brushes.Black, 100, 130);
e.Graphics.DrawString("金額:¥12,000", font, Brushes.Black, 100, 160);
}
実務では、座標を直接たくさん書くと管理しにくくなります。次のように変数で位置を管理すると読みやすくなります。
C#int left = e.MarginBounds.Left;
int top = e.MarginBounds.Top;
int lineHeight = 30;
e.Graphics.DrawString("顧客名:山田 太郎", font, Brushes.Black, left, top);
e.Graphics.DrawString("請求日:2026/06/16", font, Brushes.Black, left, top + lineHeight);
e.Graphics.DrawString("金額:¥12,000", font, Brushes.Black, left, top + lineHeight * 2);
left、top、lineHeightのような変数を使うことで、後からレイアウトを調整しやすくなります。
4-2. フォントサイズ・太字・色を指定して印刷する
フォントサイズや太字を指定するには、Fontを作成するときにスタイルを指定します。
C#using Font titleFont = new Font("Meiryo", 18, FontStyle.Bold);
using Font normalFont = new Font("Meiryo", 11);
using Brush redBrush = new SolidBrush(Color.Red);
e.Graphics.DrawString("請求書", titleFont, Brushes.Black, 100, 80);
e.Graphics.DrawString("未払い", normalFont, redBrush, 100, 130);
複数のフォントを使い分ける場合は、次のように用途ごとに名前を付けるとわかりやすいです。
C#using Font titleFont = new Font("Meiryo", 20, FontStyle.Bold);
using Font headerFont = new Font("Meiryo", 10, FontStyle.Bold);
using Font bodyFont = new Font("Meiryo", 10);
帳票では、タイトル、見出し、本文、注記などでフォントを分けると見やすくなります。
4-3. 罫線や四角形を描画する
罫線や枠線を描画するには、DrawLineやDrawRectangleを使います。
C#using Pen pen = new Pen(Color.Black, 1);
int x = 100;
int y = 150;
int width = 500;
int height = 30;
e.Graphics.DrawRectangle(pen, x, y, width, height);
e.Graphics.DrawLine(pen, x, y + height, x + width, y + height);
表のようなレイアウトを作る場合は、横線と縦線を組み合わせます。
C#int left = 100;
int top = 200;
int rowHeight = 30;
int[] colWidths = { 80, 250, 100, 100 };
int tableWidth = colWidths.Sum();
using Pen pen = new Pen(Color.Black);
for (int i = 0; i <= 5; i++)
{
int y = top + i * rowHeight;
e.Graphics.DrawLine(pen, left, y, left + tableWidth, y);
}
int currentX = left;
e.Graphics.DrawLine(pen, currentX, top, currentX, top + rowHeight * 5);
foreach (int colWidth in colWidths)
{
currentX += colWidth;
e.Graphics.DrawLine(pen, currentX, top, currentX, top + rowHeight * 5);
}
表の描画では、列幅と行高を配列や定数で管理すると拡張しやすくなります。
4-4. 画像やロゴを印刷する
画像を印刷するには、Image.FromFile()で画像を読み込み、DrawImageで描画します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Image logo = Image.FromFile(@"C:\Images\logo.png");
e.Graphics.DrawImage(logo, 100, 80, 160, 60);
}
画像を元の縦横比を保って印刷したい場合は、比率を計算します。
C#using Image logo = Image.FromFile(@"C:\Images\logo.png");
int targetWidth = 160;
int targetHeight = logo.Height * targetWidth / logo.Width;
e.Graphics.DrawImage(logo, 100, 80, targetWidth, targetHeight);
画像ファイルが存在しないと例外が発生するため、実務ではFile.Exists()で確認したり、例外処理を入れたりすると安全です。
C#string path = @"C:\Images\logo.png";
if (File.Exists(path))
{
using Image logo = Image.FromFile(path);
e.Graphics.DrawImage(logo, 100, 80, 160, 60);
}
4-5. 表形式のデータを印刷する
表形式のデータを印刷する場合は、ヘッダー行、明細行、罫線、文字位置を分けて考えます。
C#private void PrintTable(PrintPageEventArgs e)
{
using Font headerFont = new Font("Meiryo", 10, FontStyle.Bold);
using Font bodyFont = new Font("Meiryo", 10);
using Pen pen = new Pen(Color.Black);
int left = e.MarginBounds.Left;
int top = e.MarginBounds.Top;
int rowHeight = 30;
int colNo = 50;
int colName = 250;
int colQty = 80;
int colPrice = 100;
int x = left;
int y = top;
// ヘッダー
e.Graphics.DrawRectangle(pen, x, y, colNo, rowHeight);
e.Graphics.DrawString("No", headerFont, Brushes.Black, x + 5, y + 7);
x += colNo;
e.Graphics.DrawRectangle(pen, x, y, colName, rowHeight);
e.Graphics.DrawString("商品名", headerFont, Brushes.Black, x + 5, y + 7);
x += colName;
e.Graphics.DrawRectangle(pen, x, y, colQty, rowHeight);
e.Graphics.DrawString("数量", headerFont, Brushes.Black, x + 5, y + 7);
x += colQty;
e.Graphics.DrawRectangle(pen, x, y, colPrice, rowHeight);
e.Graphics.DrawString("金額", headerFont, Brushes.Black, x + 5, y + 7);
// 明細
string[,] rows =
{
{ "1", "ノート", "2", "300" },
{ "2", "ペン", "5", "750" },
{ "3", "ファイル", "1", "500" }
};
y += rowHeight;
for (int i = 0; i < rows.GetLength(0); i++)
{
x = left;
e.Graphics.DrawRectangle(pen, x, y, colNo, rowHeight);
e.Graphics.DrawString(rows[i, 0], bodyFont, Brushes.Black, x + 5, y + 7);
x += colNo;
e.Graphics.DrawRectangle(pen, x, y, colName, rowHeight);
e.Graphics.DrawString(rows[i, 1], bodyFont, Brushes.Black, x + 5, y + 7);
x += colName;
e.Graphics.DrawRectangle(pen, x, y, colQty, rowHeight);
e.Graphics.DrawString(rows[i, 2], bodyFont, Brushes.Black, x + 5, y + 7);
x += colQty;
e.Graphics.DrawRectangle(pen, x, y, colPrice, rowHeight);
e.Graphics.DrawString(rows[i, 3], bodyFont, Brushes.Black, x + 5, y + 7);
y += rowHeight;
}
}
このサンプルは基本形ですが、実務では列定義をクラス化し、右寄せや中央揃え、折り返しなどを共通処理にすると管理しやすくなります。
4-6. DataGridViewやリストデータを印刷する考え方
DataGridViewを印刷する場合、「DataGridViewそのものを印刷する」のではなく、「DataGridViewに表示しているデータを取り出して、表として描画する」と考えるのが基本です。
C#foreach (DataGridViewRow row in dataGridView1.Rows)
{
if (row.IsNewRow) continue;
string name = row.Cells["ProductName"].Value?.ToString() ?? "";
string qty = row.Cells["Quantity"].Value?.ToString() ?? "";
string price = row.Cells["Price"].Value?.ToString() ?? "";
// 取得した値をDrawStringで印刷する
}
ただし、印刷処理が画面のDataGridViewに強く依存すると、保守しにくくなります。おすすめは、印刷用のデータクラスやリストを作ってから印刷する方法です。
C#public class PrintRow
{
public string ProductName { get; set; } = "";
public int Quantity { get; set; }
public decimal Price { get; set; }
}
C#private List<PrintRow> rows = new List<PrintRow>();
画面表示用データと印刷用データを分けておくと、プレビュー、再印刷、CSV出力、PDF出力などにも対応しやすくなります。
5. 複数ページ印刷の実装方法
5-1. HasMorePagesの役割
複数ページ印刷では、e.HasMorePagesが重要です。
e.HasMorePages = true;を設定すると、まだ次のページがあると判断され、PrintPageイベントが再度呼び出されます。falseにすると印刷処理は終了します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
// 現在ページを描画
if (次のページが必要)
{
e.HasMorePages = true;
}
else
{
e.HasMorePages = false;
}
}
ポイントは、PrintPageイベントが呼ばれるたびに「次の1ページ分だけ」を描画することです。すべてのページを一度に描こうとすると、ページ分割がうまくいきません。
5-2. 複数ページ印刷の基本フロー
複数ページ印刷では、現在どこまで印刷したかをフィールドで保持します。
C#private int currentIndex = 0;
private List<string> lines = new List<string>();
印刷開始前に状態を初期化します。
C#private void buttonPrint_Click(object sender, EventArgs e)
{
currentIndex = 0;
lines = GetPrintLines();
printDocument.Print();
}
PrintPageでは、印刷できる範囲まで描画し、残りがあればHasMorePagesをtrueにします。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 10);
int y = e.MarginBounds.Top;
int lineHeight = font.Height + 5;
while (currentIndex < lines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(lines[currentIndex], font, Brushes.Black, e.MarginBounds.Left, y);
y += lineHeight;
currentIndex++;
}
e.HasMorePages = false;
}
この形が複数ページ印刷の基本です。
5-3. 長文テキストをページ分割して印刷する
長文テキストを印刷する場合は、行単位に分割してから印刷すると扱いやすくなります。
C#private List<string> printLines = new List<string>();
private int currentLine = 0;
private void PrepareText(string text)
{
printLines = text
.Replace("\r\n", "\n")
.Split('\n')
.ToList();
currentLine = 0;
}
印刷処理です。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 10);
int x = e.MarginBounds.Left;
int y = e.MarginBounds.Top;
int lineHeight = font.Height + 4;
while (currentLine < printLines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(printLines[currentLine], font, Brushes.Black, x, y);
y += lineHeight;
currentLine++;
}
e.HasMorePages = false;
}
ただし、このサンプルは「改行済みテキスト」を印刷する場合の基本です。横幅に応じて自動折り返ししたい場合は、DrawStringにRectangleFを指定するか、MeasureStringで文字サイズを測りながら行分割する必要があります。
5-4. 一覧表を複数ページに分けて印刷する
一覧表を複数ページに分ける場合も、現在の行インデックスを保持します。
C#private int currentRowIndex = 0;
private List<PrintRow> printRows = new List<PrintRow>();
印刷開始前に初期化します。
C#private void StartPrint()
{
currentRowIndex = 0;
printRows = GetRows();
printDocument.Print();
}
PrintPageでは、ヘッダーを描画してから明細を描画します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
using Font headerFont = new Font("Meiryo", 10, FontStyle.Bold);
using Font bodyFont = new Font("Meiryo", 10);
using Pen pen = new Pen(Color.Black);
int left = e.MarginBounds.Left;
int y = e.MarginBounds.Top;
int rowHeight = 28;
DrawTableHeader(e.Graphics, pen, headerFont, left, y, rowHeight);
y += rowHeight;
while (currentRowIndex < printRows.Count)
{
if (y + rowHeight > e.MarginBounds.Bottom)
{
e.HasMorePages = true;
return;
}
DrawTableRow(e.Graphics, pen, bodyFont, printRows[currentRowIndex], left, y, rowHeight);
y += rowHeight;
currentRowIndex++;
}
e.HasMorePages = false;
}
複数ページの表印刷では、各ページにヘッダー行を出すと見やすくなります。また、ページ下部に合計欄を出すか、最終ページだけに出すかなど、業務要件に合わせて設計しましょう。
5-5. ページ番号を印刷する方法
ページ番号を印刷するには、現在ページ番号をフィールドで保持します。
C#private int currentPage = 1;
印刷開始前に初期化します。
C#private void buttonPrint_Click(object sender, EventArgs e)
{
currentPage = 1;
currentRowIndex = 0;
printDocument.Print();
}
フッターにページ番号を描画します。
C#private void DrawFooter(PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 9);
string pageText = $"{currentPage} ページ";
SizeF size = e.Graphics.MeasureString(pageText, font);
float x = e.MarginBounds.Right - size.Width;
float y = e.MarginBounds.Bottom + 20;
e.Graphics.DrawString(pageText, font, Brushes.Black, x, y);
}
PrintPageの最後でページ番号を進めます。
C#DrawFooter(e);
if (hasMore)
{
currentPage++;
e.HasMorePages = true;
}
else
{
e.HasMorePages = false;
}
総ページ数も表示したい場合は、事前にページ数を計算する処理が必要です。単純な一覧表であれば、1ページあたりの行数から総ページ数を算出できます。
C#int totalPages = (int)Math.Ceiling((double)printRows.Count / rowsPerPage);
ただし、折り返し行や可変高さの明細がある場合は、実際にレイアウト計算しないと正確な総ページ数を求めにくいです。
5-6. 印刷状態を保持・初期化するタイミング
複数ページ印刷でよくある失敗が、印刷状態の初期化忘れです。
たとえば、currentRowIndexを初期化しないまま再印刷すると、前回の続きから印刷されてしまい、白紙になったり途中から始まったりします。
印刷状態は、次のタイミングで初期化するのが基本です。
・印刷ボタンを押した直後
・プレビューを表示する直前
・再印刷を開始する直前
例:
C#private void ResetPrintState()
{
currentRowIndex = 0;
currentPage = 1;
}
C#private void buttonPreview_Click(object sender, EventArgs e)
{
ResetPrintState();
using PrintPreviewDialog dialog = new PrintPreviewDialog();
dialog.Document = printDocument;
dialog.ShowDialog();
}
プレビューでもPrintPageイベントは呼ばれます。そのため、プレビュー後に本印刷する場合も、印刷状態を再度初期化する必要があります。
6. 印刷プレビューの表示方法
6-1. PrintPreviewDialogを使った印刷プレビューの基本
印刷プレビューを表示するには、PrintPreviewDialogを使います。
C#using PrintPreviewDialog previewDialog = new PrintPreviewDialog();
previewDialog.Document = printDocument;
previewDialog.ShowDialog();
PrintPreviewDialogは、PrintDocumentの描画内容を画面上で確認するためのダイアログです。実務アプリでは、いきなり印刷するよりもプレビューを表示してから印刷できるようにすることが多いです。
6-2. PrintDocumentをPrintPreviewDialogに設定する
PrintPreviewDialogを使うときは、DocumentプロパティにPrintDocumentを設定します。
C#private void buttonPreview_Click(object sender, EventArgs e)
{
ResetPrintState();
using PrintPreviewDialog previewDialog = new PrintPreviewDialog();
previewDialog.Document = printDocument;
previewDialog.Width = 1000;
previewDialog.Height = 800;
previewDialog.ShowDialog();
}
ResetPrintState()は、複数ページ印刷の状態を初期化するためのメソッドです。
C#private void ResetPrintState()
{
currentRowIndex = 0;
currentPage = 1;
}
プレビュー表示でもPrintPageイベントが呼ばれるため、印刷状態の初期化は必ず行いましょう。
6-3. プレビューから印刷まで行う実装例
PrintPreviewDialogには印刷ボタンが表示されるため、プレビュー画面から印刷することができます。
C#private void buttonPreview_Click(object sender, EventArgs e)
{
ResetPrintState();
using PrintPreviewDialog previewDialog = new PrintPreviewDialog
{
Document = printDocument,
Width = 1000,
Height = 800
};
previewDialog.ShowDialog();
}
ただし、プレビュー画面から印刷した場合、プレビュー生成時に進んだ印刷状態が影響することがあります。特に、currentRowIndexやcurrentPageをフィールドで管理している場合は注意が必要です。
安全にするには、BeginPrintイベントで初期化する方法もあります。
C#public Form1()
{
InitializeComponent();
printDocument.BeginPrint += PrintDocument_BeginPrint;
printDocument.PrintPage += PrintDocument_PrintPage;
}
private void PrintDocument_BeginPrint(object sender, PrintEventArgs e)
{
currentRowIndex = 0;
currentPage = 1;
}
BeginPrintは印刷処理の開始時に呼ばれるため、プレビューでも本印刷でも状態を初期化しやすくなります。
6-4. PrintPreviewControlを使って独自プレビュー画面を作る方法
標準のPrintPreviewDialogではなく、独自フォームにプレビューを埋め込みたい場合はPrintPreviewControlを使います。
C#PrintPreviewControl previewControl = new PrintPreviewControl();
previewControl.Document = printDocument;
previewControl.Dock = DockStyle.Fill;
this.Controls.Add(previewControl);
独自プレビュー画面を作ると、次のようなカスタマイズが可能です。
・画面上部に独自の印刷ボタンを配置する
・倍率変更ボタンを用意する
・ページ移動ボタンを作る
・検索条件や帳票条件を同じ画面に表示する
・業務システムの画面デザインに合わせる
ただし、標準のPrintPreviewDialogに比べると実装量は増えます。まずはPrintPreviewDialogを使い、必要に応じてPrintPreviewControlへ拡張するとよいでしょう。
6-5. プレビューと実際の印刷結果がずれる原因
プレビューでは正しく見えるのに、実際に印刷すると位置がずれることがあります。主な原因は次のとおりです。
・プリンタードライバーによって印刷可能領域が異なる
・用紙サイズがプリンター側の設定と一致していない
・余白がプリンター側で補正されている
・拡大縮小印刷が有効になっている
・プリンターがフチなし印刷に対応していない
・フォントが環境によって異なる
対策としては、MarginBoundsを基準に描画する、ページ端ぎりぎりに印刷しない、プリンターごとの用紙設定を確認する、テスト印刷で補正値を持つなどが有効です。
また、プレビューと本印刷で異なる描画処理を使うと、結果がずれやすくなります。プレビューでも本印刷でも、同じPrintDocumentと同じPrintPage処理を使うことが重要です。
6-6. プレビューが白紙になる場合の確認ポイント
プレビューが白紙になる場合は、次の点を確認しましょう。
・PrintPageイベントを登録しているか
・PrintPage内でe.Graphicsに描画しているか
・文字色が白になっていないか
・描画座標が印刷範囲外になっていないか
・印刷対象データが空になっていないか
・複数ページ用のインデックスが最後まで進んだままになっていないか
・例外が発生して描画処理が途中で止まっていないか
特に多いのは、イベント登録忘れです。
C#printDocument.PrintPage += PrintDocument_PrintPage;
また、プレビューを一度表示した後に再表示すると白紙になる場合は、currentRowIndexなどの印刷状態を初期化していない可能性があります。
7. プリンター選択と印刷ダイアログの使い方
7-1. PrintDialogを使ってプリンターを選択する
ユーザーにプリンターを選ばせたい場合は、PrintDialogを使います。
C#private void buttonPrint_Click(object sender, EventArgs e)
{
using PrintDialog printDialog = new PrintDialog();
printDialog.Document = printDocument;
if (printDialog.ShowDialog() == DialogResult.OK)
{
printDocument.Print();
}
}
PrintDialogを使うと、ユーザーはプリンター、部数、ページ範囲などを選択できます。業務アプリでは、固定プリンターに出す場合を除き、印刷ダイアログを表示した方が安全です。
7-2. PrinterSettingsでプリンター名を指定する
特定のプリンターへ印刷したい場合は、PrinterSettings.PrinterNameを設定します。
C#printDocument.PrinterSettings.PrinterName = "プリンター名";
指定したプリンターが有効かどうかは、IsValidで確認できます。
C#printDocument.PrinterSettings.PrinterName = "Microsoft Print to PDF";
if (printDocument.PrinterSettings.IsValid)
{
printDocument.Print();
}
else
{
MessageBox.Show("指定されたプリンターは利用できません。");
}
公式ドキュメントでも、PrinterNameを設定して対象プリンターを指定し、IsValidで有効性を確認する例が紹介されています。Microsoft Learn
プリンター名は環境によって異なるため、固定文字列で指定する場合は注意が必要です。
7-3. 既定プリンターで印刷する方法
PrinterSettings.PrinterNameを明示的に指定しない場合、通常は既定プリンターが使用されます。
C#private void buttonPrintDefault_Click(object sender, EventArgs e)
{
printDocument.Print();
}
現在のプリンター名を確認したい場合は、次のように取得できます。
C#string printerName = printDocument.PrinterSettings.PrinterName;
MessageBox.Show($"使用プリンター:{printerName}");
ただし、ユーザーの環境によって既定プリンターは異なります。業務システムで帳票専用プリンターやラベルプリンターを使う場合は、アプリ側でプリンターを選択・保存できるようにすると便利です。
7-4. 部数を指定して印刷する
部数を指定するには、PrinterSettings.Copiesを使います。
C#printDocument.PrinterSettings.Copies = 2;
printDocument.Print();
ただし、プリンターやドライバーによって部数指定の扱いが異なる場合があります。また、部単位でページをそろえる必要がある場合は、Collateの指定も検討します。
C#printDocument.PrinterSettings.Copies = 2;
printDocument.PrinterSettings.Collate = true;
業務帳票では「控え」「納品書」「請求書」など、同じ内容を複数部印刷するケースがあります。この場合、単純に部数を指定するだけでよいのか、それぞれに異なる文言を印字する必要があるのかを確認しましょう。
7-5. 印刷ページ範囲を指定する
ページ範囲を扱うには、PrinterSettingsのFromPage、ToPage、PrintRangeなどを使います。
C#printDocument.PrinterSettings.FromPage = 1;
printDocument.PrinterSettings.ToPage = 3;
printDocument.PrinterSettings.PrintRange = PrintRange.SomePages;
PrintDialogでページ範囲を指定させる場合は、次のように設定します。
C#using PrintDialog dialog = new PrintDialog();
dialog.Document = printDocument;
dialog.AllowSomePages = true;
printDocument.PrinterSettings.MinimumPage = 1;
printDocument.PrinterSettings.MaximumPage = 10;
printDocument.PrinterSettings.FromPage = 1;
printDocument.PrinterSettings.ToPage = 10;
if (dialog.ShowDialog() == DialogResult.OK)
{
printDocument.Print();
}
ただし、PrintDocumentはページ描画を自分で制御する仕組みなので、指定されたページ範囲だけを描画するロジックを自前で考慮する必要があります。ページ番号を管理して、範囲外のページでは描画をスキップするなどの工夫が必要です。
7-6. プリンターが利用可能か確認する方法
プリンターが利用可能か確認するには、PrinterSettings.IsValidを使います。
C#PrinterSettings settings = new PrinterSettings();
settings.PrinterName = "プリンター名";
if (!settings.IsValid)
{
MessageBox.Show("プリンターが見つかりません。");
}
インストール済みプリンターの一覧を取得するには、PrinterSettings.InstalledPrintersを使います。
C#foreach (string printer in PrinterSettings.InstalledPrinters)
{
comboBoxPrinters.Items.Add(printer);
}
プリンター一覧をコンボボックスに表示して、ユーザーに選択させる実装はよく使われます。
C#private void LoadPrinters()
{
comboBoxPrinters.Items.Clear();
foreach (string printer in PrinterSettings.InstalledPrinters)
{
comboBoxPrinters.Items.Add(printer);
}
comboBoxPrinters.Text = new PrinterSettings().PrinterName;
}
ネットワークプリンターやラベルプリンターは、接続状態やドライバーの状態によって利用できないことがあります。印刷前には必ずチェックする設計にしましょう。
8. 用紙サイズ・向き・余白を設定する方法
8-1. PageSettingsで用紙設定を行う基本
用紙サイズ、向き、余白などはPageSettingsで設定します。PrintDocumentでは、通常DefaultPageSettingsを変更します。
C#printDocument.DefaultPageSettings.Landscape = false;
printDocument.DefaultPageSettings.Margins = new Margins(50, 50, 50, 50);
Marginsの引数は、左、右、上、下の順です。
C#new Margins(left, right, top, bottom)
単位は一般的に1/100インチです。たとえば100は約1インチ、50は約0.5インチです。
8-2. A4・B5・はがきなどの用紙サイズを指定する
用紙サイズを指定するには、プリンターが対応しているPaperSizeを探して設定します。PaperSizeは、プリンターで利用可能な用紙サイズの取得や、ページ設定への用紙サイズ指定に使われるクラスです。Microsoft Learn
C#foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
if (size.Kind == PaperKind.A4)
{
printDocument.DefaultPageSettings.PaperSize = size;
break;
}
}
B5の場合です。
C#foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
if (size.Kind == PaperKind.B5)
{
printDocument.DefaultPageSettings.PaperSize = size;
break;
}
}
はがきの場合は、プリンターやドライバーによってPaperKindや用紙名が異なることがあります。PaperNameを確認して選ぶ方法もあります。
C#foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
if (size.PaperName.Contains("はがき") || size.PaperName.Contains("Postcard"))
{
printDocument.DefaultPageSettings.PaperSize = size;
break;
}
}
用紙サイズはプリンターによって対応状況が異なるため、固定で指定する前に利用可能な用紙一覧を確認することが重要です。
8-3. 横向き・縦向きを切り替える
横向きに印刷するには、Landscapeをtrueにします。
C#printDocument.DefaultPageSettings.Landscape = true;
縦向きにする場合はfalseです。
C#printDocument.DefaultPageSettings.Landscape = false;
一覧表の列数が多い場合は横向き、請求書や納品書のような帳票は縦向きにすることが多いです。
C#private void SetLandscape(bool isLandscape)
{
printDocument.DefaultPageSettings.Landscape = isLandscape;
}
横向きにするとMarginBoundsの幅と高さも変わるため、レイアウト計算は固定値ではなく、e.MarginBounds.Widthやe.MarginBounds.Heightを使って行うと安全です。
8-4. 余白を設定する
余白を設定するには、Marginsを使います。
C#printDocument.DefaultPageSettings.Margins = new Margins(50, 50, 50, 50);
これは、左、右、上、下にそれぞれ50を指定する例です。
C#Margins margins = new Margins(
left: 80,
right: 80,
top: 100,
bottom: 100
);
printDocument.DefaultPageSettings.Margins = margins;
余白を小さくしすぎると、プリンターの物理的な印刷不可領域にかかり、文字や線が切れることがあります。特にレシートプリンター、ラベルプリンター、古いプリンターでは、端まで印刷できないケースが多いです。
実務では、余白を完全にゼロにするのではなく、少し余裕を持たせることをおすすめします。
8-5. カスタム用紙サイズを指定する
ラベルやレシートなどで独自サイズを使う場合は、PaperSizeを作成して設定します。
C#PaperSize customSize = new PaperSize("Custom", 400, 600);
printDocument.DefaultPageSettings.PaperSize = customSize;
PaperSizeの幅と高さも、基本的には1/100インチ単位です。
400 = 4インチ
600 = 6インチ
ただし、カスタム用紙サイズはプリンター側で対応していないと反映されない場合があります。ラベルプリンターやレシートプリンターでは、ドライバー側に用紙サイズを登録しておく必要があることもあります。
確実に運用するには、次の点を確認しましょう。
・プリンタードライバーがカスタム用紙に対応しているか
・Windowsのプリンター設定に用紙サイズを登録しているか
・アプリ側の用紙サイズとドライバー側の用紙サイズが一致しているか
・余白設定が用紙サイズに対して大きすぎないか
8-6. プリンターごとに使える用紙サイズを取得する
プリンターが対応している用紙サイズは、PrinterSettings.PaperSizesで取得できます。
C#foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
Console.WriteLine($"{size.PaperName} / {size.Width} x {size.Height} / {size.Kind}");
}
Windows Formsでは、コンボボックスに表示してユーザーに選ばせることもできます。
C#private void LoadPaperSizes()
{
comboBoxPaperSizes.Items.Clear();
foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
comboBoxPaperSizes.Items.Add(size);
}
comboBoxPaperSizes.DisplayMember = "PaperName";
}
選択された用紙サイズを設定します。
C#if (comboBoxPaperSizes.SelectedItem is PaperSize selectedSize)
{
printDocument.DefaultPageSettings.PaperSize = selectedSize;
}
プリンターによって利用できる用紙は大きく異なります。A4プリンター、ラベルプリンター、レシートプリンターでは用紙一覧がまったく違うため、実務ではプリンター選択後に用紙サイズ一覧を再読み込みする設計が向いています。
8-7. QueryPageSettingsでページごとに設定を変える
ページごとに用紙設定を変えたい場合は、QueryPageSettingsイベントを使います。公式ドキュメントでは、QueryPageSettingsによって各ページに異なるページ設定を適用でき、変更は現在ページにのみ影響すると説明されています。Microsoft Learn
C#printDocument.QueryPageSettings += PrintDocument_QueryPageSettings;
C#private int pageNumber = 1;
private void PrintDocument_QueryPageSettings(object sender, QueryPageSettingsEventArgs e)
{
if (pageNumber == 1)
{
e.PageSettings.Landscape = false;
}
else
{
e.PageSettings.Landscape = true;
}
}
QueryPageSettingsは、各ページのPrintPageイベントの直前に呼ばれます。表紙は縦向き、明細ページは横向きにするなど、ページ単位で設定を変えたい場合に便利です。
ただし、ページ番号の管理とPrintPage側の状態管理が複雑になりやすいため、必要な場合に限定して使うのがおすすめです。
9. 実務でよく使うPrintDocumentの応用テクニック
9-1. 帳票レイアウトをきれいに整える設計方法
帳票レイアウトをきれいに整えるには、固定値を直接書きすぎないことが重要です。
悪い例です。
C#e.Graphics.DrawString("請求書", font, Brushes.Black, 123, 45);
e.Graphics.DrawString("顧客名", font, Brushes.Black, 87, 132);
e.Graphics.DrawLine(Pens.Black, 90, 180, 720, 180);
このように座標を直接書くと、後からレイアウト変更が難しくなります。
おすすめは、基準位置とサイズを変数化する方法です。
C#int left = e.MarginBounds.Left;
int top = e.MarginBounds.Top;
int pageWidth = e.MarginBounds.Width;
int titleY = top;
int customerY = top + 60;
int tableY = top + 140;
さらに、帳票の領域を分けると管理しやすくなります。
・タイトル領域
・宛先領域
・発行者領域
・明細表領域
・合計領域
・フッター領域
それぞれをメソッド化すると、読みやすく保守しやすいコードになります。
C#DrawTitle(e.Graphics, e.MarginBounds);
DrawCustomerInfo(e.Graphics, e.MarginBounds);
DrawDetailTable(e.Graphics, e.MarginBounds);
DrawTotal(e.Graphics, e.MarginBounds);
DrawFooter(e.Graphics, e.MarginBounds);
9-2. レシート・ラベル・バーコード印刷での注意点
レシートやラベル印刷では、A4帳票とは違う注意点があります。
・用紙幅が小さい
・余白がプリンター依存になりやすい
・カスタム用紙設定が必要になる
・プリンタードライバーの拡大縮小設定に影響されやすい
・バーコードは解像度やサイズが重要
・印刷位置の微調整が必要になりやすい
レシートでは、文字サイズを小さくしすぎると読みにくくなります。ラベルでは、数ミリのずれでも印字位置が大きく見えるため、テスト印刷を行いながら調整することが重要です。
バーコードを印刷する場合は、バーコード画像を生成してDrawImageで印刷する方法が一般的です。
C#using Image barcode = Image.FromFile(@"C:\Images\barcode.png");
e.Graphics.DrawImage(barcode, 100, 200, 240, 80);
バーコードは横方向に無理に縮小すると読み取り精度が落ちる場合があります。バーコード規格やリーダーの読み取り条件に合わせてサイズを決めましょう。
9-3. 右寄せ・中央揃え・折り返し文字を印刷する
右寄せや中央揃えを行うには、StringFormatを使います。
中央揃えの例です。
C#using StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
Rectangle rect = new Rectangle(100, 100, 300, 40);
e.Graphics.DrawRectangle(Pens.Black, rect);
e.Graphics.DrawString("中央揃え", font, Brushes.Black, rect, format);
右寄せの例です。
C#using StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Far;
format.LineAlignment = StringAlignment.Center;
Rectangle rect = new Rectangle(100, 150, 300, 40);
e.Graphics.DrawString("12,000", font, Brushes.Black, rect, format);
表の金額列は右寄せにすると見やすくなります。
折り返し文字を印刷する場合は、DrawStringに矩形を指定します。
C#RectangleF rect = new RectangleF(100, 200, 300, 100);
e.Graphics.DrawString(
"長い文章を指定した幅の中で折り返して印刷します。",
font,
Brushes.Black,
rect
);
セル内で折り返しを使う場合は、行の高さも可変にする必要があります。MeasureStringで必要な高さを測定してから罫線を描画するときれいに印刷できます。
9-4. 改ページ位置を自動計算する
改ページ位置を自動計算するには、現在のY座標とMarginBounds.Bottomを比較します。
C#if (currentY + rowHeight > e.MarginBounds.Bottom)
{
e.HasMorePages = true;
return;
}
可変高さの行を扱う場合は、先に行の高さを計算します。
C#SizeF textSize = e.Graphics.MeasureString(text, font, columnWidth);
int rowHeight = Math.Max(30, (int)Math.Ceiling(textSize.Height) + 10);
if (currentY + rowHeight > e.MarginBounds.Bottom)
{
e.HasMorePages = true;
return;
}
改ページの基本は、「描画する前に、その領域がページ内に収まるか確認する」ことです。
C#if (描画予定の下端 > 印刷可能領域の下端)
{
次ページへ送る
}
else
{
現在ページに描画する
}
これを徹底すると、表の途中で罫線だけ次ページに出る、文字がページ下部で切れる、といった問題を防ぎやすくなります。
9-5. ヘッダー・フッターを全ページに印刷する
ヘッダーやフッターを全ページに印刷するには、PrintPageイベントが呼ばれるたびに描画します。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
DrawHeader(e);
DrawBody(e);
DrawFooter(e);
}
ヘッダーの例です。
C#private void DrawHeader(PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 14, FontStyle.Bold);
e.Graphics.DrawString(
"売上一覧表",
font,
Brushes.Black,
e.MarginBounds.Left,
e.MarginBounds.Top
);
e.Graphics.DrawLine(
Pens.Black,
e.MarginBounds.Left,
e.MarginBounds.Top + 35,
e.MarginBounds.Right,
e.MarginBounds.Top + 35
);
}
フッターの例です。
C#private void DrawFooter(PrintPageEventArgs e)
{
using Font font = new Font("Meiryo", 9);
string text = $"印刷日:{DateTime.Now:yyyy/MM/dd}";
e.Graphics.DrawString(
text,
font,
Brushes.Black,
e.MarginBounds.Left,
e.MarginBounds.Bottom + 20
);
}
本文の開始位置は、ヘッダーの高さを考慮して下げます。
C#int bodyTop = e.MarginBounds.Top + 60;
ヘッダーとフッターの領域を固定しておくと、本文と重ならずに印刷できます。
9-6. 印刷前にレイアウトを確認しやすくする工夫
印刷レイアウトは、実際に紙へ出す前にプレビューで確認できるようにすると効率的です。
おすすめの工夫は次のとおりです。
・必ずプレビュー画面を用意する
・デバッグ時だけ枠線を表示する
・MarginBoundsを可視化する
・ページ番号を表示する
・印刷対象データ件数をログ出力する
・用紙サイズとプリンター名を画面に表示する
デバッグ時に余白領域を描画する例です。
C##if DEBUG
e.Graphics.DrawRectangle(Pens.Red, e.MarginBounds);
#endif
表やラベル印刷では、少しのずれが問題になるため、デバッグ用の補助線を表示できるようにしておくと便利です。
また、プレビューと本印刷で別々の処理を書かないことも重要です。同じ描画メソッドを使うことで、「プレビューでは正しいのに印刷結果が違う」という問題を減らせます。
10. PrintDocumentでよくあるエラーと対処法
10-1. 印刷されない・白紙になる原因
PrintDocumentで印刷されない、または白紙になる場合、原因の多くは描画処理にあります。
確認すべきポイントは次のとおりです。
・PrintPageイベントを登録しているか
・PrintPageイベント内でDrawStringなどを呼んでいるか
・描画座標がページ外になっていないか
・文字色が背景色と同じになっていないか
・印刷データが空ではないか
・HasMorePagesの制御が間違っていないか
・例外が発生して処理が中断していないか
特に、イベント登録忘れはよくあります。
C#printDocument.PrintPage += PrintDocument_PrintPage;
また、PrintPageの中で何も描画していない場合も白紙になります。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
// 何も描画していないため白紙になる
}
必ずe.Graphicsに対して描画してください。
10-2. PrintPageイベントが呼ばれない原因
PrintPageイベントが呼ばれない場合は、次の点を確認します。
・Print()を呼んでいるか
・PrintPreviewDialogにDocumentを設定しているか
・PrintPageイベントを登録しているか
・PrintDocumentのインスタンスが別物になっていないか
・印刷前に例外が発生していないか
よくあるミスは、イベントを登録したPrintDocumentとは別のインスタンスを印刷しているケースです。
C#// イベント登録したインスタンス
printDocument.PrintPage += PrintDocument_PrintPage;
// しかし別インスタンスを作って印刷してしまう
PrintDocument other = new PrintDocument();
other.Print();
この場合、otherにはPrintPageイベントが登録されていないため、期待した処理が呼ばれません。
フォームのフィールドとして1つのPrintDocumentを管理し、プレビューも印刷も同じインスタンスを使うようにしましょう。
10-3. プリンター名が無効になる場合の対処
プリンター名を指定しても無効になる場合は、PrinterSettings.IsValidを確認します。
C#printDocument.PrinterSettings.PrinterName = printerName;
if (!printDocument.PrinterSettings.IsValid)
{
MessageBox.Show("指定されたプリンターが見つかりません。");
return;
}
原因としては、次のようなものがあります。
・プリンター名が間違っている
・プリンターが削除された
・ネットワークプリンターに接続できない
・ドライバーがインストールされていない
・ユーザー環境によってプリンター名が違う
プリンター名を設定ファイルに保存する場合は、アプリ起動時や印刷前に現在も有効か確認しましょう。無効な場合は、プリンター選択画面を表示する設計にすると親切です。
10-4. 用紙サイズや余白設定が反映されない原因
用紙サイズや余白が反映されない場合は、次の点を確認します。
・プリンターが指定した用紙サイズに対応しているか
・DefaultPageSettingsに設定しているか
・PrintDialog側で設定が上書きされていないか
・プリンタードライバー側の設定と競合していないか
・カスタム用紙サイズがドライバーに登録されているか
・余白がプリンターの印刷可能領域より小さすぎないか
用紙サイズは、プリンターが対応しているPaperSizesから選ぶのが基本です。
C#foreach (PaperSize size in printDocument.PrinterSettings.PaperSizes)
{
if (size.Kind == PaperKind.A4)
{
printDocument.DefaultPageSettings.PaperSize = size;
break;
}
}
カスタム用紙を使う場合は、アプリ側だけでなく、プリンタードライバー側の設定も確認してください。
10-5. プレビューでは正しいのに印刷結果がずれる原因
プレビューと実際の印刷結果がずれる場合は、プリンター固有の印刷可能領域やドライバー設定が影響していることが多いです。
主な原因です。
・プリンターの物理的な非印刷領域
・ドライバー側の拡大縮小設定
・用紙トレイの設定違い
・フチなし印刷設定の有無
・プリンターごとの余白補正
・DPIやフォントレンダリングの違い
対策としては、ページ端ぎりぎりに配置しないこと、MarginBoundsを基準にすること、テスト印刷で補正値を調整することが有効です。
また、プレビューと印刷で異なるプリンター設定を使っているとずれやすくなります。プレビュー前と印刷前で、同じPrinterSettings、PageSettingsを使うようにしましょう。
10-6. 文字化け・フォント崩れが起きる場合の対処
文字化けやフォント崩れが起きる場合は、フォント指定を見直します。
C#using Font font = new Font("Meiryo", 10);
日本語を印刷する場合は、Meiryo、Yu Gothic、MS Gothicなど、日本語に対応したフォントを指定します。環境によって存在しないフォントを指定すると、代替フォントが使われ、見た目が変わることがあります。
対策は次のとおりです。
・日本語対応フォントを指定する
・対象PCにフォントが存在するか確認する
・特殊記号や外字を避ける
・バーコードフォントを使う場合はインストール状態を確認する
・環境依存文字を使わない
フォントの存在を確認したい場合は、InstalledFontCollectionを使う方法もあります。
C#using System.Drawing.Text;
InstalledFontCollection fonts = new InstalledFontCollection();
bool exists = fonts.Families.Any(f => f.Name == "Meiryo");
業務システムでは、開発環境ではきれいに印刷できても、利用者PCではフォントが違って崩れることがあります。運用環境でのテストが重要です。
10-7. 例外発生時に確認すべきポイント
印刷処理では、プリンター、ファイル、画像、フォント、権限など複数の要因で例外が発生します。
例外処理の基本です。
C#try
{
printDocument.Print();
}
catch (Exception ex)
{
MessageBox.Show($"印刷中にエラーが発生しました。\n{ex.Message}");
}
確認すべきポイントは次のとおりです。
・プリンターが有効か
・画像ファイルのパスが正しいか
・ファイルにアクセス権があるか
・用紙サイズが対応しているか
・PrintPage内でNullReferenceExceptionが起きていないか
・Dispose済みのFontやImageを使っていないか
・ネットワークプリンターに接続できるか
特にPrintPageイベント内の例外は見落としがちです。デバッグ時はPrintPage内にもログやブレークポイントを置いて確認しましょう。
11. PrintDocument実装時の注意点とベストプラクティス
11-1. 印刷処理と画面表示処理を分離する
実務でPrintDocumentを使う場合、印刷処理と画面表示処理は分離した方が保守しやすくなります。
悪い例です。
C#string name = textBoxName.Text;
string amount = labelAmount.Text;
e.Graphics.DrawString(name, font, Brushes.Black, 100, 100);
e.Graphics.DrawString(amount, font, Brushes.Black, 100, 130);
画面コントロールから直接値を取得すると、画面変更の影響を印刷処理が受けやすくなります。
おすすめは、印刷用のデータクラスを作る方法です。
C#public class InvoicePrintData
{
public string CustomerName { get; set; } = "";
public DateTime IssueDate { get; set; }
public List<InvoiceDetail> Details { get; set; } = new();
}
印刷処理は、このデータを受け取って描画します。
C#private InvoicePrintData printData;
このようにすると、画面表示、印刷、PDF出力、テストが分離しやすくなります。
11-2. レイアウト計算を共通化する
印刷レイアウトが複雑になるほど、座標計算を共通化することが重要です。
たとえば、列幅を定義するクラスを作ります。
C#public class ColumnDefinition
{
public string Header { get; set; } = "";
public int Width { get; set; }
public StringAlignment Alignment { get; set; }
}
列定義をリストで持ちます。
C#private readonly List<ColumnDefinition> columns = new()
{
new ColumnDefinition { Header = "No", Width = 50, Alignment = StringAlignment.Center },
new ColumnDefinition { Header = "商品名", Width = 250, Alignment = StringAlignment.Near },
new ColumnDefinition { Header = "数量", Width = 80, Alignment = StringAlignment.Far },
new ColumnDefinition { Header = "金額", Width = 100, Alignment = StringAlignment.Far }
};
このように設計すると、表の列追加や幅変更に強くなります。
また、文字描画やセル描画をメソッド化すると、帳票全体の統一感を保ちやすくなります。
C#private void DrawCell(Graphics g, string text, Font font, Rectangle rect, StringAlignment alignment)
{
using StringFormat format = new StringFormat
{
Alignment = alignment,
LineAlignment = StringAlignment.Center
};
g.DrawRectangle(Pens.Black, rect);
g.DrawString(text, font, Brushes.Black, rect, format);
}
11-3. プレビューと本印刷で同じ描画処理を使う
プレビューと本印刷で別々の描画処理を書くと、表示と印刷がずれる原因になります。
よくない例です。
C#// プレビュー用
DrawPreviewLayout();
// 印刷用
DrawPrintLayout();
このように分けると、片方だけ修正してもう片方を忘れる可能性があります。
おすすめは、PrintPage内の描画処理を共通化し、プレビューも本印刷も同じPrintDocumentを使うことです。
C#private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
DrawReport(e.Graphics, e.MarginBounds);
}
C#private void DrawReport(Graphics g, Rectangle bounds)
{
DrawTitle(g, bounds);
DrawBody(g, bounds);
DrawFooter(g, bounds);
}
同じ描画処理を使えば、プレビューで確認した内容に近い結果を印刷できます。
11-4. Disposeが必要なオブジェクトを正しく扱う
印刷ではFont、Pen、Brush、Imageなどを多く使います。自分で生成したオブジェクトは、usingで確実に破棄しましょう。
C#using Font font = new Font("Meiryo", 10);
using Pen pen = new Pen(Color.Black);
using Brush brush = new SolidBrush(Color.Black);
画像も同様です。
C#using Image image = Image.FromFile(path);
e.Graphics.DrawImage(image, rect);
ただし、e.Graphicsは自分で破棄してはいけません。PrintPageEventArgsから渡されるGraphicsは、印刷システム側が管理しています。
C#// これはしない
e.Graphics.Dispose();
Disposeの基本は次のとおりです。
自分でnewしたもの:Disposeする
FromFileなどで作ったもの:Disposeする
PrintPageEventArgsから受け取ったGraphics:Disposeしない
Pens.BlackやBrushes.Black:Disposeしない
11-5. 大量データ印刷時のパフォーマンス対策
大量データを印刷する場合は、パフォーマンスにも注意が必要です。
対策としては、次のようなものがあります。
・印刷前に必要なデータだけを取得する
・PrintPage内で重いDBアクセスをしない
・画像をページごとに何度も読み込まない
・フォントやペンを必要以上に作成しない
・レイアウト計算を効率化する
・ログを出しすぎない
特に避けたいのは、PrintPageイベント内でデータベースを毎回検索することです。
C#// 避けたい例
private void PrintDocument_PrintPage(object sender, PrintPageEventArgs e)
{
var rows = LoadRowsFromDatabase();
}
印刷前にデータを取得しておき、PrintPageでは描画に集中するのが基本です。
C#private List<PrintRow> printRows;
private void StartPrint()
{
printRows = LoadRowsFromDatabase();
currentRowIndex = 0;
printDocument.Print();
}
画像やロゴも、毎ページImage.FromFile()で読み込むより、印刷開始前に読み込むか、必要な範囲でキャッシュする方が効率的です。
11-6. ユーザー環境によるプリンター差異を考慮する
印刷機能は、ユーザー環境の影響を受けやすい処理です。同じコードでも、プリンター、ドライバー、用紙、フォント、Windows設定によって結果が変わることがあります。
考慮すべき差異は次のとおりです。
・プリンター名
・対応用紙サイズ
・印刷可能領域
・余白の扱い
・解像度
・フォント
・ネットワーク接続
・ドライバーのバージョン
そのため、実務では次のような設計が有効です。
・プリンター選択画面を用意する
・用紙サイズをプリンターから取得する
・プレビュー機能を用意する
・印刷位置の補正値を設定できるようにする
・エラー時にプリンター名や用紙情報を表示する
・運用環境のプリンターで必ずテストする
帳票印刷では、開発PCだけでなく、実際に利用するプリンターでテストすることが重要です。
12. C# PrintDocumentに関するよくある質問
12-1. PrintDocumentはコンソールアプリでも使える?
技術的には、System.Drawing.Printingを参照すればコンソールアプリからPrintDocumentを使える場合があります。ただし、PrintDocumentは主にWindows Formsの印刷機能として使われることが多く、印刷ダイアログやプレビューを使う場合はWindows Formsの仕組みが必要になります。
コンソールアプリで単純に既定プリンターへ印刷するだけなら、次のようなコードは可能です。
C#using System.Drawing;
using System.Drawing.Printing;
PrintDocument doc = new PrintDocument();
doc.PrintPage += (s, e) =>
{
using Font font = new Font("Meiryo", 12);
e.Graphics.DrawString("コンソールアプリから印刷", font, Brushes.Black, 100, 100);
};
doc.Print();
ただし、ユーザー操作を伴う印刷には向きません。業務アプリでプレビューやプリンター選択が必要なら、Windows Formsアプリとして実装する方が扱いやすいです。
12-2. WPFでPrintDocumentは使える?
WPFアプリでも、参照を追加すればSystem.Drawing.Printing.PrintDocumentを使うことはできます。ただし、WPFにはPrintDialogやDocumentPaginatorなど、WPF向けの印刷機能もあります。
WPF画面やFlowDocumentを印刷したい場合は、WPF標準の印刷機能を検討した方が自然です。一方、既存のWindows Forms向け印刷ロジックを流用したい場合や、Graphicsベースで帳票を描画したい場合は、PrintDocumentを使う選択肢もあります。
ただし、WPFとWindows Formsでは描画の考え方が異なります。WPFのVisualをそのままPrintDocumentで印刷するのではなく、Graphicsに描画する処理を別途用意する必要があります。
12-3. PDFとして出力できる?
PrintDocument自体には、PDFを直接生成する機能はありません。ただし、Windowsの「Microsoft Print to PDF」のようなPDF仮想プリンターを選択すれば、印刷先としてPDFを作成できます。
C#printDocument.PrinterSettings.PrinterName = "Microsoft Print to PDF";
ただし、PDFの保存先ファイル名を完全に自動制御するには追加の対応が必要になる場合があります。PDFを安定して生成したい場合は、PDF生成ライブラリを使う方が向いています。
たとえば、次のような要件がある場合は、PrintDocumentよりPDFライブラリの方が適しています。
・サーバー上でPDFを生成したい
・ファイル名を自動指定したい
・WebアプリでPDFを出力したい
・検索可能なテキストPDFを作りたい
・帳票をメール添付したい
PrintDocumentは「プリンターへ出力する」仕組みであり、「PDFファイルを生成する」専用機能ではない点を理解しておきましょう。
12-4. 印刷前に必ずプレビューを表示できる?
できます。印刷ボタンを押したときに直接Print()を呼ぶのではなく、PrintPreviewDialogを表示すれば、印刷前にプレビューを確認できます。
C#private void buttonPrint_Click(object sender, EventArgs e)
{
ResetPrintState();
using PrintPreviewDialog dialog = new PrintPreviewDialog();
dialog.Document = printDocument;
dialog.ShowDialog();
}
プレビュー画面から印刷できるため、ユーザーに確認してから印刷させることができます。
ただし、プレビューを表示するたびにPrintPageイベントが呼ばれるため、複数ページ印刷では状態管理に注意してください。BeginPrintイベントで初期化する設計にしておくと、プレビューと本印刷の両方で安定しやすくなります。
12-5. プリンターなしで動作確認できる?
物理プリンターがなくても、PDF仮想プリンターやXPS仮想プリンターを使えば動作確認できます。
代表的な方法は次のとおりです。
・Microsoft Print to PDFを使う
・Microsoft XPS Document Writerを使う
・PrintPreviewDialogでプレビュー確認する
プレビューだけなら紙に印刷しなくてもレイアウト確認ができます。ただし、最終的な印字位置、余白、用紙送り、ラベル位置、バーコード読み取りなどは実プリンターで確認する必要があります。
特にラベルプリンターやレシートプリンターは、仮想プリンターでは再現できない差異が多いため、実機テストが重要です。
12-6. .NET 6以降でもPrintDocumentは使える?
Windows向けのWindows Formsアプリケーションであれば、.NET 6以降でもPrintDocumentは利用できます。ただし、System.Drawing.Commonは.NET 6以降、Windows以外のOSではサポート対象外であり、非Windows環境では注意が必要です。Microsoft Learn+1
.NET 6以降でWindows Formsとして使う場合は、ターゲットフレームワークをWindows向けにします。
XML<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
クロスプラットフォーム対応が必要なアプリでは、PrintDocumentに依存した設計は避けた方がよいでしょう。PDF生成、Web印刷、各OSの印刷API、帳票ライブラリなど、目的に合った別の方法を検討する必要があります。
まとめ
C#のPrintDocumentは、Windows Formsアプリケーションで印刷機能を実装するための基本的かつ強力なクラスです。文字、線、図形、画像、表などをGraphicsに描画することで、帳票、ラベル、レシート、一覧表などさまざまな印刷に対応できます。
基本の流れは次のとおりです。
1. PrintDocumentを作成する
2. PrintPageイベントを登録する
3. PrintPage内でe.Graphicsに描画する
4. PrintPreviewDialogでプレビューする
5. PrintDialogやPrinterSettingsでプリンターを設定する
6. Print()で印刷を実行する
複数ページ印刷では、HasMorePagesと現在行インデックスの管理が重要です。用紙サイズ、向き、余白はPageSettingsで設定し、プリンター選択や部数指定はPrinterSettingsで行います。
実務では、プレビューと本印刷で同じ描画処理を使うこと、印刷状態を正しく初期化すること、座標やレイアウト計算を共通化することが大切です。また、プリンターやドライバーによって印刷結果が変わることがあるため、実際の運用環境でテストすることも欠かせません。
PrintDocumentは、画面をそのまま印刷する簡易機能ではなく、紙面に対して自分で描画する仕組みです。その分、座標、フォント、罫線、画像、ページ分割を理解すれば、業務アプリに必要な柔軟な印刷機能を実装できます。

