更新履歴(6件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- `PrintableArea`との重なりを取る例で、座標系の変換が抜けていました。`MarginBounds`も`PrintableArea`も用紙の左上からの座標ですが、`OriginAtMargins`が既定の`false`のとき`Graphics`の原点は印字可能な範囲の左上にあります。そのまま描くとハードマージンのぶんだけ右下へずれ、右端と下端が欠けます ── この例が防ごうとしている症状そのものでした。描く前に`Graphics`の座標へ移すようにし、`OriginAtMargins`を変えると補正も変わることを明記しました。
- 余白の図で`MarginBounds`をハードマージンの内側に入れ子で描いていたのを直しました。`MarginBounds`は`Margins`だけから計算され、印字可能な範囲へ丸められません。`Margins`をハードマージンより小さくすると印字可能な範囲からはみ出すため、入れ子として読むと欠ける原因を取り違えます。2つが別々に決まる図に描き直し、`PrintableArea`との重なりを取るコード例と、`OriginAtMargins`の既定が`false`で原点の決定に`Margins`が使われないことを追記しました。
- 選択肢を絞り込む決定木と実装前チェックをまとめとして新設しました。あわせてGDIとXPSの印刷経路の図、余白の入れ子の図、ライブラリを比較する手順、用語の説明を追加しています。
- 印刷座標の単位の説明にあった誤記を「100分の1インチ単位(hundredths of an inch)」に修正しました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589955)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windows業務アプリの印刷とPDF出力 ── System.Drawing.Printing / WPF / 帳票ライブラリの使い分け」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589955 https://staging.comcomponent.com/blog/windows-app-printing-pdf-guide/
- DOI(最新版)
- 10.5281/zenodo.21589955
- DOI(この版)
- 10.5281/zenodo.21732938
Windows業務アプリの印刷は、「PrintDocumentで適当に描画すれば終わり」という話にはなりません。改ページが効かない、余白がずれる、プレビューと実際の出力が違う、PDFだけ欲しいのに印刷ダイアログが挟まる ── こうした問題は、たいてい印刷の仕組みそのものを誤解したまま実装を始めたことが原因です。
この記事では、次の4つの選択肢を、要件別にどう使い分けるかという観点で整理します。以降、この番号で参照します。
| # | 選択肢 | 主に扱う章 |
|---|---|---|
| ① | WinFormsのSystem.Drawing.Printing(PrintDocument) |
3章 |
| ② | WPFのFlowDocument / FixedDocument |
4章 |
| ③ | PDF出力ライブラリでの直接生成 | 5章 |
| ④ | Excel / Word経由の帳票(Open XML SDK、COM自動化) | 6章 |
1. まず結論
- 簡単な一覧や既存WinForms資産の延長なら
PrintDocumentで十分です。 改ページはPrintPageイベントをHasMorePagesがfalseになるまで繰り返し呼び出す仕組みで、これを忘れると2ページ目以降が出力されません。12 - 印刷のDPIは画面のDPIと別物です。
PageSettings.PrinterResolutionはプリンターの解像度を表し、余白はPageSettings.HardMarginX/HardMarginY(プリンターが物理的に持つ印字不可能領域)とMargins(アプリが指定する論理的な余白)の2段構えになっています。画面の座標をそのまま印刷に流用すると、この2つのDPI・座標系の違いでレイアウトがずれます。345 - WPFではWindows標準の印刷パスがGDI印刷パスとXPS印刷パスの2系統に分かれています。 WPFアプリケーションは本来XPS印刷パスを使い、XPSDrv非対応のプリンターに送る場合は自動的にGDI形式へ変換されます。67
- WPFの文書は「流し込み」の
FlowDocumentと「固定レイアウト」のFixedDocumentで設計思想が違います。FlowDocumentは可読性優先でウィンドウサイズや解像度に応じて再レイアウトされ、FixedDocumentは表示・印刷デバイスの忠実度を優先する構成です。89 - 用紙やトレイの指定はWPFの
PrintDialogとPrintTicket/PrintQueueで行います。 印刷前にPrintQueue.MergeAndValidatePrintTicketでプリンターの実際の能力に対して検証・マージするのが定石です。1011 - Microsoft Print to PDFは対話的な操作を前提にした印刷ドライバーです。 OSに標準搭載された印刷キューとドライバーパッケージという位置づけで、実行のたびに保存先を選ぶダイアログが挟まる構成が基本であり、無人バッチ処理には向きません。12
- サーバーやサービスからのOffice COM自動化は、Microsoftが公式に非サポート・非推奨としています。 Officeは対話的なデスクトップとユーザープロファイルを前提に設計されており、無人・非対話環境では不安定な動作やデッドロックを起こし得ます。13
- Windowsサービスはセッション0で動作するため、ダイアログを表示できず、ユーザーのセッションに紐づく既定プリンターにも依存できません。 印刷を伴う常駐処理を設計する際は、この境界を最初に意識してください。14
- 長期稼働でGDIオブジェクト(ハンドル)を解放し忘れると、プロセスあたりの上限に達してクラッシュします。 上限はレジストリの
GDIProcessHandleQuotaで調整可能な有限の値であり、無限に確保できるわけではありません。15
判断表 ── 要件 × 手段
| 要件 | 適した手段 | 選択肢 | 理由 |
|---|---|---|---|
| 簡単な一覧・伝票を数枚印刷したい(WinForms) | PrintDocument |
① | 標準クラスのみで完結し、既存のGDI+描画資産をそのまま流用できる |
| 罫線の多い帳票、複数用紙・複雑なレイアウト | 帳票ライブラリ、またはFixedDocumentでの固定レイアウト |
②または③ | 座標計算・改ページ制御・日本語組版を自前で書くコストが大きい |
| 大量バッチ印刷(夜間無人実行) | プログラム的なPrintDocument.Print()/XpsDocumentWriterへの直接送信 |
①または② | ダイアログを一切出さず完結させる必要があり、既定プリンター依存も避けたい |
| PDF保存だけでよい(印刷は使わない) | PDF生成ライブラリで直接PDFを作る | ③ | 印刷ダイアログや既定プリンター、スプーラーの状態に依存しない |
| プレビューが必須、社内の確認フローが重要 | PrintPreviewDialog(WinForms)、DocumentViewer(WPF) |
①または② | 印刷前確認の標準UIがそのまま使える |
| 既存Excelテンプレートを流用したい | Open XML SDK等での直接生成、または限定的なCOM自動化 | ④ | 資産を再利用できるが、常駐サービスでのCOM自動化は避ける |
以下、各項目の詳細を見ていきます。
この記事の知識マップ
Windows業務アプリの印刷は、画面がWinFormsならPrintDocument、WPFなら流し込み優先のFlowDocumentか固定レイアウト優先のFixedDocumentを使うのが素直で、それぞれGDI印刷パスとXPS印刷パスという別経路でスプーラーへ送られます。余白はMarginBoundsとプリンター側のハードマージンが別々に決まり、両者を意識せず描画すると印刷の端が欠けます。無人バッチでPDFだけを生成したいなら、対話的な保存先選択が前提のMicrosoft Print to PDFではなくPDF生成ライブラリでの直接生成が向きます。サーバーサイドやWindowsサービスでのOffice COM自動化はMicrosoftが非サポートと明言しており不安定な動作やデッドロックを招き得るため、常駐処理からの印刷はセッション0の制約とあわせて設計する必要があります。
flowchart LR
accTitle: Windows業務アプリの印刷・PDF出力の知識マップ
accDescr: PrintDocument・FixedDocument・FlowDocument・PDF生成ライブラリがGDI印刷パスとXPS印刷パスのどちらを通るか、余白とハードマージンのずれが印刷の端の欠けを生む仕組み、無人バッチではMicrosoft Print to PDFではなくPDFライブラリが向くこと、サービスやOffice COM自動化の制約の関係を示す図。
printdocument["PrintDocument(WinFormsの印刷)"]
fixeddocument["FixedDocument"]
pdf_generation_library["PDF生成ライブラリ"]
gdi_print_path["GDI印刷パス"]
xps_print_path["XPS印刷パス"]
flowdocument["FlowDocument"]
print_spooler["印刷スプーラー"]
margin_bounds["MarginBounds"]
hard_margin["ハードマージン"]
print_edge_clipping["印刷の端の欠け"]
printable_area["PrintableArea(印字可能な範囲)"]
microsoft_print_to_pdf["Microsoft Print to PDF"]
unattended_batch_output["無人バッチでのPDF大量生成"]
office_com_automation["サーバーサイドOffice COM自動化"]
windows_service["Windowsサービス"]
deadlock["デッドロック(deadlock)"]
com["COM(コンポーネントオブジェクトモデル)"]
session_zero["セッション0"]
interactive_dialog_hang["対話的な待ちによるハング"]
gdi_handle_leak["GDIハンドルリーク"]
windows_forms["Windows Forms"]
printdocument -->|"利用する"| gdi_print_path
fixeddocument -->|"利用する"| xps_print_path
flowdocument -->|"利用する"| xps_print_path
xps_print_path -.->|"利用する"| gdi_print_path
gdi_print_path -->|"利用する"| print_spooler
xps_print_path -->|"利用する"| print_spooler
printdocument -->|"利用する"| margin_bounds
hard_margin -.->|"原因になり得る"| print_edge_clipping
margin_bounds -.->|"原因になり得る"| print_edge_clipping
printable_area -.->|"防止する"| print_edge_clipping
microsoft_print_to_pdf -->|"利用する"| print_spooler
microsoft_print_to_pdf -->|"用いるのは非推奨"| unattended_batch_output
pdf_generation_library -->|"推奨される対応"| unattended_batch_output
office_com_automation -->|"用いるのは非推奨"| windows_service
office_com_automation -->|"原因になり得る"| deadlock
office_com_automation -->|"利用する"| com
windows_service -->|"利用する"| session_zero
session_zero -->|"原因になり得る"| interactive_dialog_hang
printdocument -->|"原因になり得る"| gdi_handle_leak
printdocument -->|"推奨される対応"| windows_forms
office_com_automation -->|"用いるのは非推奨"| unattended_batch_output
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. Windowsの印刷の仕組みを最低限だけ
この章から使う用語を先に定義しておきます。
| 用語 | 意味 |
|---|---|
| DPI(dots per inch) | 1インチあたりのドット数。解像度の単位。画面は通常96 DPI前後、プリンターは600 DPI前後と桁が違う |
| スプーラー | アプリから受け取った印刷ジョブを一旦ためて、順にプリンタードライバーへ渡すWindowsのサービス。プリンターが止まってもアプリ側は先に進める仕組み |
| EMF(拡張メタファイル) | GDIの描画命令を記録したファイル形式。GDI印刷パスでは、この形でスプーラーへ渡される |
| WYSIWYG(What You See Is What You Get) | 画面で見えているものが、そのまま出力される状態のこと。印刷では「プレビューと実際の紙が一致する」ことを指す |
| セッション0 | Windows Vista以降、サービスなど対話的ユーザーに紐付かないプロセス専用に予約されたセッション。ログオン中ユーザーの画面とは分離されている14 |
| ハードマージン | プリンターが物理的に印字できない、用紙の端の帯。アプリからは狭められない |
Windowsの印刷アーキテクチャは、印刷スプーラーと各プリンター用のドライバーで構成されています。アプリケーションはデバイスに依存しない関数を呼ぶだけで、レーザープリンター、ベクタープロッター、ラスタープリンター、FAXなど多様な出力先に印刷ジョブを送れます。16
印刷パスは大きく2系統あります。
- GDI印刷パス。 Win32 GDIアプリケーションが印刷すると、GDIグラフィックスエンジンが描画命令をEMF(拡張メタファイル)としてスプールするか、プリンタードライバーと連携して印刷可能なイメージを直接レンダリングし、スプーラーへ送ります。従来のWinFormsアプリはこちらの経路を通ります。166
- XPS印刷パス。 XPS(XML Paper Specification)ベースのプリンタードライバー向けの経路で、WPFアプリケーションは本来こちらを使い、XPSドキュメントの形式でスプーラーに送ります。対象プリンターがXPSDrvドライバーでなければ、GDI形式に自動変換されてから送られます。67
図にすると次のようになります。どちらの経路を通っても、最終的にはスプーラーとドライバーを経由するという点は共通です。
flowchart LR
WF["WinFormsアプリ<br/>選択肢① PrintDocument"] --> GDI["GDI印刷パス<br/>描画命令をEMFとしてスプール"]
WPF["WPFアプリ<br/>選択肢② FlowDocument・FixedDocument"] --> XPS["XPS印刷パス<br/>XPSドキュメントとしてスプール"]
XPS -->|"XPSDrv対応のプリンター"| SPL["印刷スプーラー"]
XPS -->|"XPSDrv非対応のプリンター"| CONV["GDI形式へ自動変換"]
CONV --> SPL
GDI --> SPL
SPL --> DRV["プリンタードライバー"]
DRV --> OUT["プリンター・FAX<br/>Microsoft Print to PDF など"]
図1: GDI印刷パスとXPS印刷パス。WPFからXPSDrv非対応のプリンターへ送ると、途中でGDI形式に変換される
なぜ画面の見た目と印刷結果がずれるのか。 一番よくある原因はDPIの取り違えです。画面のGraphicsは通常96 DPI前後で座標系が組まれていますが、プリンターのGraphicsはPageSettings.PrinterResolutionで決まる、多くの場合600 DPI前後のはるかに高い解像度で動きます。3 画面用に計算したピクセル座標をそのまま印刷用のGraphicsに渡すと、意図よりずっと小さく(あるいは高解像度設定によっては大きく)描画されます。印刷では常に100分の1インチ単位(hundredths of an inch)や実寸(ミリ・インチ)を基準に座標を組み立て、Graphicsの解像度に依存しない設計にするのが安全です。
もう一つ見落としやすいのが、プリンターが持つ物理的な印字不可能領域です。多くのプリンターは用紙の端から数ミリの範囲に印刷できません。この物理制約はPageSettings.HardMarginX/HardMarginYで取得でき、公式ドキュメントでも「プリンターが設定する物理的な余白を表す」と説明されています。4 アプリ側で指定する論理的な余白はPageSettings.Margins(既定は上下左右1インチ)ですが、これを0に設定しても、プリンターのハードマージンより内側には出力できません。5 「余白を0にしたのに端が切れる/思ったよりずれる」という不具合の多くは、このハードマージンの存在を意識していないことが原因です。
ここで大事なのは、この2つが別々に決まることです。ハードマージンはプリンターが決め、MarginBounds はアプリが指定した Margins だけから計算されます。MarginBounds が印字可能な範囲へ自動で丸められることはありません。関係を図にすると、入れ子ではなく、こうなります。
flowchart TB
subgraph PB["PageBounds ── 用紙全体のサイズ"]
HM["印字可能な範囲<br/>PrintableArea・HardMarginX・HardMarginY<br/>プリンターが決める。アプリからは狭められない"]
MB["MarginBounds<br/>Margins だけから計算される<br/>印字可能な範囲へは丸められない"]
end
HM --> SAFE["確実に出る範囲 ── 2つが重なっている部分"]
MB --> SAFE
図2: MarginBounds とハードマージンは別々に決まる。Margins をハードマージンより小さくすると、MarginBounds は印字可能な範囲からはみ出す
- PageBounds: 用紙全体のサイズ。
- ハードマージン: プリンターが決める物理的な印字不可能領域。
HardMarginX/HardMarginYで取得でき、アプリからは狭められません。4 印字可能な範囲そのものはPageSettings.PrintableAreaで取れます(100分の1インチ単位)。17 - MarginBounds: アプリが指定した
Margins(既定は上下左右1インチ)を反映した描画領域。5
つまり、Margins を0にしたときのように論理的な余白がハードマージンより小さいと、MarginBounds は印字可能な範囲の外へはみ出します。その状態で MarginBounds を基準に描くと、はみ出した帯の中身は印刷されずに欠けます。「余白を0にしたのに端が切れる」の正体はこれです。
安全側に倒すには、次のどちらかを取ります。
- 論理的な余白を、ハードマージン以上にしておく。
Marginsの各辺をHardMarginX/HardMarginYと比べ、小さいほうを持ち上げます - 描画に使う矩形を
PrintableAreaと重ねて求める。MarginBoundsをそのまま使わず、Rectangle.Intersectで印字可能な範囲との共通部分を取ります
// PrintPage イベント内。単位はすべて100分の1インチ
private void OnPrintPage(object? sender, PrintPageEventArgs e)
{
PageSettings page = e.PageSettings;
// 用紙の左上から見た「印字可能な範囲」。左上の位置はハードマージンそのもの
var printableOnPaper = new RectangleF(
page.HardMarginX, page.HardMarginY,
page.PrintableArea.Width, page.PrintableArea.Height);
// MarginBounds は Margins からの計算値で、印字可能な範囲へは丸められていない。
// 重なりを取れば、ハードマージンが大きい機種でも欠けない範囲が出る。
// ここまでは「用紙の左上」を原点とした座標
Rectangle safeOnPaper = Rectangle.Intersect(
e.MarginBounds, Rectangle.Round(printableOnPaper));
// ここが要注意。OriginAtMargins が既定の false のとき、e.Graphics の原点は
// 「印字可能な範囲の左上」にあり、用紙の左上ではありません。用紙座標のまま
// 描くと、ハードマージンのぶんだけ右下へずれ、右端と下端が印字可能な範囲から
// はみ出して欠けます ── この例が防ごうとしている症状そのものです。
// 描く前に Graphics の座標へ移します
Rectangle safe = safeOnPaper;
safe.Offset(
-(int)Math.Round(page.HardMarginX),
-(int)Math.Round(page.HardMarginY));
e.Graphics!.DrawRectangle(Pens.Black, safe);
}
OriginAtMargins = true にした場合は原点の位置が変わるので、この補正も変わります。どちらで描くかを先に決めてから座標を組み立ててください。混ぜると、機種を変えたときだけ位置がずれる、という追いにくい不具合になります。
もう1つ、座標の原点にも注意が必要です。PrintDocument.OriginAtMargins の既定は false で、このとき Graphics の原点は印字可能な範囲の左上に置かれ、Margins の値は原点の決定に使われません。18 「Margins を設定したのに、その分だけずれてくれない」あるいは逆に「余計にずれる」と感じたら、まずここを疑います。原点を余白の内側に置きたいなら OriginAtMargins = true を明示します。
3. WinForms: System.Drawing.Printingの実務
WinFormsの印刷はPrintDocumentコンポーネントを中心に組み立てます。基本の流れは、PrintDocumentを作り、PrinterSettingsやDefaultPageSettingsを設定し、PrintPageイベントで実際の描画を行い、Print()メソッドで印刷を開始するというものです。1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
public sealed class ReportPrinter
{
// 印刷対象データ。1行1件の伝票明細を想定
private readonly IReadOnlyList<string> _lines;
private int _lineIndex;
public ReportPrinter(IReadOnlyList<string> lines) => _lines = lines;
public void Print(string printerName)
{
using var document = new PrintDocument();
document.DocumentName = "受注明細";
if (!string.IsNullOrEmpty(printerName))
{
document.PrinterSettings.PrinterName = printerName;
}
// 未知のプリンター名を指定した場合にここでfalseになる(オフライン検知には使えない)
if (!document.PrinterSettings.IsValid)
{
throw new InvalidOperationException(
$"指定したプリンターが見つかりません: {printerName}");
}
_lineIndex = 0;
document.PrintPage += Document_PrintPage;
document.Print();
}
private void Document_PrintPage(object? sender, PrintPageEventArgs e)
{
// MarginBoundsは論理的な余白(Margins)を反映した描画可能領域。
// PageBoundsは用紙全体で、ハードマージンより内側に描いても切れる
using var font = new Font("メイリオ", 10);
float lineHeight = font.GetHeight(e.Graphics);
float y = e.MarginBounds.Top;
while (_lineIndex < _lines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
// このページはここまで。続きがあるので次ページへ
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(_lines[_lineIndex], font, Brushes.Black,
e.MarginBounds.Left, y);
y += lineHeight;
_lineIndex++;
}
// 全行を描き終えたので、これが最後のページ
e.HasMorePages = false;
}
}
「2ページ目が出ない」不具合の典型パターンは、HasMorePagesを明示的にfalseにし忘れる(既定値はfalse)か、逆に条件分岐の書き方を誤って常にfalseのまま抜けてしまうケースです。PrintPageイベントは、このプロパティがfalseになるまで繰り返し呼び出される仕組みであることを踏まえ、「まだ描くべき行が残っているか」を明示的に判定してから設定してください。12 ページごとに異なる設定(用紙サイズや向きなど)を反映したい場合は、PrintPageに加えてQueryPageSettingsイベントも活用できます。
「余白がずれる」不具合の典型パターンは、e.Graphics.VisibleClipBoundsやe.PageBoundsを基準に座標を組み立ててしまい、MarginBounds(論理余白を反映した領域)とHardMarginX/HardMarginY(プリンターの物理制約)を無視することです。4 特に、開発機のプリンターではハードマージンが小さくたまたま問題が出ず、納品先の別機種でハードマージンが大きく、罫線や項目が欠けて見える、という事故が起きやすいところです。複数機種での印刷検証は、開発時に見落とされがちな工程です。
プレビューを提供する場合はPrintPreviewDialogを使い、Documentプロパティに同じPrintDocumentを割り当てるだけで、PrintPageイベントのロジックをそのまま再利用できます。1 プリンター解像度に応じた出力を切り替えたい場合は、PrinterSettings.PrinterResolutionsから選択肢を取得し、PageSettings.PrinterResolutionに設定します。3
4. WPF: FlowDocument / FixedDocumentとPrintDialog
WPFでは、文書の性質によってFlowDocumentとFixedDocumentのどちらを軸にするかが変わります。
FlowDocument。 ウィンドウサイズやフォント設定など実行時の変数に応じて内容を動的に再レイアウトする、可読性優先の文書です。検索、ページング、表示モード変更などの機能を標準で持ちます。8FixedDocument。 アプリケーション側がレイアウトを完全に制御する、忠実度優先のWYSIWYG文書です。表示・印刷デバイスに対して高精度な再現性が必要な場合に向きます。9
流し込みで読みやすさを優先する社内向けレポートならFlowDocument、罫線位置や項目位置をピクセル単位で固定したい伝票・ラベルならFixedDocument、という判断が基本線です。
基本的な印刷はSystem.Windows.Controls.PrintDialogで行います。このクラスはSystem.Windows.Forms.PrintDialogとは別物で、WPF専用です。10
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public static class FlowDocumentPrinter
{
public static void Print(FlowDocument document, string jobName)
{
var printDialog = new PrintDialog();
// ユーザーがOKを押した場合のみ印刷する
if (printDialog.ShowDialog() != true)
{
return;
}
// FlowDocumentは表示用と印刷用でページサイズの前提が異なるため、
// 印刷対象のページサイズ・余白をプリンターの印刷可能領域に合わせる
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = new Size(
printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
printDialog.PrintDocument(paginator, jobName);
}
}
用紙サイズ・給紙トレイ・両面印刷などを細かく指定したい場合は、PrintQueueとPrintTicket/PrintCapabilitiesを使います。PrintTicketは印刷ジョブへの指示、PrintCapabilitiesはプリンターが実際にサポートする機能を表し、指定した設定はPrintQueue.MergeAndValidatePrintTicketでプリンター固有の妥当な値にマージ・検証してから使うのが定石です。711
using System.Linq;
using System.Printing;
public static class DuplexPrintTicketBuilder
{
public static PrintTicket BuildDuplexTicket(PrintQueue printQueue)
{
PrintCapabilities capabilities = printQueue.GetPrintCapabilities();
// 両面印刷(長辺綴じ)に対応しているプリンターかどうかを事前に確認する
bool supportsDuplex = capabilities.DuplexingCapability
.Contains(Duplexing.TwoSidedLongEdge);
var requestedTicket = new PrintTicket();
if (supportsDuplex)
{
requestedTicket.Duplexing = Duplexing.TwoSidedLongEdge;
}
// ユーザーの既定PrintTicketに対して、両面印刷の要求だけをマージする。
// 未対応の項目は妥当な値に自動的に置き換えられる
ValidationResult result = printQueue.MergeAndValidatePrintTicket(
printQueue.UserPrintTicket, requestedTicket);
return result.ValidatedPrintTicket;
}
}
固定レイアウトの帳票をFixedDocumentで組んでXPS印刷パスに直接送る場合は、XpsDocumentWriterのWrite/WriteAsyncメソッドでPrintQueueにジョブを追加します。対象がXPSDrv非対応のプリンターであれば、内部でGDI形式に自動変換されてから送られるため、呼び出し側で経路を意識する必要はありません。7
5. PDF出力の選択肢
「PDFで出したい」という要件は、実は次の3つのどれを指しているかで設計が変わります。
- 印刷の延長として、ユーザーが手動でPDF保存を選べればよい
- アプリ内から、印刷ダイアログを介してPDFを生成したい
- 印刷は一切せず、無人バッチでPDFファイルだけを大量生成したい
Microsoft Print to PDFは1と2向けの機能です。 Windows 10以降に標準搭載された、印刷キューとドライバーパッケージという位置づけの機能で、印刷対象アプリからは通常のプリンターの1つとして見えます。12 ただし実体は「印刷するたびに保存先ファイル名を尋ねる」という対話的な動作が前提の仕組みであり、公式にも印刷キューと当該ドライバーパッケージ自体を管理者がイメージから除去できる、という単位でしか制御手段が提供されていません。12 無人・大量のバッチ処理でファイルパスを固定して静かにPDFを吐き出す、という3の用途には設計上向いていません。3を実現したい場合は、印刷パイプラインを経由せず、PDFを直接生成するライブラリを使うのが素直です。
PDF生成ライブラリを選ぶときの比較観点は、特定の製品を推す前に、次の軸で自社の要件を整理しておくと選定がぶれません。
| 観点 | 確認すること |
|---|---|
| ライセンス | OSS(MIT/Apache系か、GPL/AGPL系か)か商用ライセンスか。自社アプリを社外に配布・販売する場合、ライブラリのライセンス条件(ソースコード開示義務の有無、再頒布条件、商用利用の可否)が自社の配布形態と矛盾しないかを必ず一次情報(ライセンス条文・提供元の公式サイト)で確認する |
| 日本語フォントの扱い | 日本語フォントのサブセット埋め込みに対応しているか、埋め込みなしでPDF側にフォントの実体を持たせられるか(閲覧環境にフォントがない場合に文字化けしないか)、縦書きや異体字(IVS)が必要な業務ならその対応状況 |
| 帳票向け機能 | テンプレートベースでの帳票定義に対応しているか、バーコード・2次元コードの生成、電子署名、PDF/A(長期保存規格)対応、既存の紙帳票のレイアウトをそのまま移植しやすいか |
| 依存関係・実行環境 | サーバー環境(GUIなしのWindows Server、コンテナー)でも動作するか、ネイティブライブラリへの依存があるか、.NET Core/.NET(非Framework)環境での動作実績 |
| パフォーマンス | 大量ページ・大量ジョブのバッチ生成時のメモリ使用量、並列生成時の挙動 |
ライセンス条件は製品やバージョンによって変わるため、必ず提供元の公式ドキュメントやライセンス条文そのものを確認してから採用判断をしてください。この記事では特定のライブラリ名や製品の推奨は行わず、比較の「観点」と、その情報をどこで確認するかを示します。
候補を探して比較する手順は次のとおりです。観点だけ持って検索しても、記事の書かれた時期によって結論が変わってしまうため、一次情報にあたる順番を決めておくのが確実です。
| 段階 | 見る場所 | 具体的に見るもの |
|---|---|---|
| 1. 候補を挙げる | NuGet Gallery の pdf タグ、Categories(Tags)検索 |
ダウンロード数の桁。桁が2つ以上違うなら、採用実績の差として意味がある |
| 2. 生きているか確認する | NuGetのパッケージページ「Version History」 | 直近1年以内にリリースがあるか、リリース間隔が空きすぎていないか |
| 3. 開発の実態を見る | GitHubリポジトリ(NuGetページの「Source repository」リンク) | 直近のコミット日、Open Issue の滞留具合、Issueに対するメンテナーの返信の有無 |
| 4. .NETの対応を確認する | NuGetページの「Frameworks」欄 | 自社が使うnet8.0などのターゲットが対象に入っているか。net472しかないものは.NET(Core系)では使えない |
| 5. ライセンスを確認する | NuGetページの「License」リンク先の条文そのもの | 商用配布の可否、ソースコード開示義務の有無。要約記事ではなく条文を読む |
| 6. 実物で試す | 小さな検証プロジェクト | 日本語フォントの埋め込み、想定ページ数での生成時間とメモリ使用量 |
特に見落とされやすいのが4と5です。日本語の紹介記事を見て決めたあとで、「.NET Frameworkにしか対応していなかった」「AGPLだったので自社製品に組み込めなかった」と分かるのは、検証工数を丸ごと捨てることになります。候補を1つに絞る前に、4と5だけ先に通すと手戻りが減ります。
6. Excel / Word経由の帳票という選択肢
すでにExcelテンプレートで運用している帳票がある場合、それをゼロから作り直すより、既存資産を活かす構成の方が現実的なことがあります。方式は大きく2つです。
- Open XML SDK等でxlsx/docxを直接生成・編集する。 Excel/Wordの実行ファイルを起動せず、ファイル形式(ZIP + XML)を直接読み書きするため、サーバー環境でも安定して動作します。テンプレートの罫線・書式をそのまま活かしつつ、値だけを差し込む構成と相性がよく、詳しくは「Excel帳票出力の作り方 - COM/Open XML/テンプレート」に整理しています。
- Excel COM Interopで実際にExcel.Applicationを操作する。 マクロや複雑な再計算を伴う既存Excelブックをそのまま使い回したい場合に選ばれますが、COM参照の解放漏れによりEXCEL.EXEプロセスが残り続ける問題が起きやすく、対策と判断基準は「C#のExcel操作でEXCEL.EXEが残る問題」で扱っています。
ここで重要なのは、サーバーサイドやWindowsサービスでのOffice COM自動化は、Microsoftが公式に「非推奨・非サポート」と明言していることです。Officeアプリケーションは対話的なデスクトップとログオン済みユーザーのプロファイルを前提に設計されており、無人・非対話環境で実行すると不安定な動作やデッドロックを起こし得るとされています。13 ライセンス上も、サーバー側の自動化でOffice機能をクライアントに提供する使い方は、通常のEULAでは想定されていません。13
したがって、帳票出力を常駐サービスやバッチジョブに組み込みたい場合は、たとえ既存のExcelテンプレートを流用するとしても、実行時に本当にExcel.exeを起動する構成(COM Interop)は避け、Open XML SDK等によるファイル直接生成に寄せるのが安全です。どうしてもCOM Interopが必要な場合は、対話的にログオンしたユーザーセッションで動く常駐アプリケーション(サービスではない)として構成する、という切り分けが現実的です。
7. 実務の落とし穴
Windowsサービス・セッション0からの印刷
印刷処理を常駐サービスに組み込みたいという相談は少なくありませんが、Windowsサービスはセッション0で動作します。セッション0は、Windows Vista以降サービスや対話的ユーザーと関わらないプロセス専用に予約されたセッションで、ユーザーの対話的セッションとは分離されています。14 この分離により、サービスはダイアログボックスを表示してもユーザーには見えず、ユーザーの入力を待つコード(印刷ダイアログの表示待ちなど)を書くとハングしたように見えます。14
さらに実務上の落とし穴になりやすいのが、ネットワークプリンターやユーザー個別にマップされたプリンターの見え方です。ユーザーがログオンして接続したネットワークプリンターは、そのユーザーの対話的セッションに紐づいて見えることが多く、別セッション(セッション0で動くサービスのプロセスコンテキスト)からは同じようには見えません。印刷を伴う常駐処理は、「どのアカウント・どのセッションから、どのプリンターが見えるか」を設計の最初に確認する必要があります。セッション分離の全体像は「Windowsのセッション分離をどう理解するか」に、サービスとしての実装・運用の基礎は「Windowsサービスの作り方と運用」に整理しています。
プリンター不在・オフライン時の挙動
PrinterSettings.PrinterNameに存在しないプリンター名を指定したり、既定プリンターが未設定の環境で実行したりすると、印刷を試みた時点で例外や失敗が起きます。PrinterSettings.IsValidで事前に検証してからにする、印刷失敗を想定した再試行・エラー通知の設計をしておく、という2点は最低限押さえておきたいところです。ネットワークプリンターが一時的にオフラインの場合、スプーラーにジョブは積まれても実際の出力は止まったままになることがあるため、印刷結果を「送信できた」で終わらせず、必要に応じてジョブの状態を確認する仕組みも検討してください。
既定プリンター依存の危険
PrinterSettings.PrinterNameを明示的に指定せず、常に「既定のプリンター」に頼る実装は、運用が長くなるほどリスクになります。ユーザーが別のプリンターを既定に変更する、ノートPCを別の場所に持ち出して既定プリンターが変わる、といった環境変化で意図しない出力先に送られてしまうためです。業務上重要な帳票は、設定ファイルやアプリ内の設定画面で印刷先プリンター名を明示的に保持し、既定プリンターの変化に引きずられない設計にしておくべきです。
長期稼働でのGDIハンドルリーク
Graphics、Font、Brush、PenなどのGDI/GDI+リソースをusingやDispose()で確実に解放しないコードを印刷処理の中に書くと、長期稼働後にGDIオブジェクトのハンドルが枯渇し、描画関連のAPI呼び出しが失敗し始めます。GDIオブジェクトのハンドルはプロセスごとに既定の上限があり、レジストリのGDIProcessHandleQuotaで256から65,536の範囲で調整できる、有限のリソースです。15 印刷を頻繁に行う常駐アプリケーションほど、この種のリークが本番環境でしか顕在化しない不具合になりがちです。ハンドルリークの調べ方・切り分け方は、産業用カメラの長期稼働クラッシュ事例を扱った「産業用カメラ長期稼働クラッシュ調査 - ハンドルリーク編」で具体的に解説しています。
8. まとめ
決定木 ── 4つの選択肢のどれを選ぶか
冒頭に挙げた①〜④は、次の順に問いを立てれば絞り込めます。
- 印刷は本当に必要か。 出力物がPDFファイルだけでよく、紙に出すのは利用者が任意でやることなら、印刷パイプラインを経由しない③(PDFライブラリでの直接生成)が最も素直です。Microsoft Print to PDFは保存先を尋ねる対話的な動作が前提で、無人バッチには向きません(5章)。
- 既存のExcel/Wordテンプレートを流用したいか。 そうなら④。ただし常駐サービスやバッチからのCOM自動化はMicrosoftが非サポートと明言しているため、Open XML SDK等での直接生成に寄せます(6章)。
- 画面がWinFormsかWPFか。 既存アプリのフレームワークに合わせます。WinFormsなら①、WPFなら②。技術を混在させる必要はありません(3章・4章)。
- WPFを選んだなら、流し込みか固定レイアウトか。 可読性優先の社内レポートは
FlowDocument、罫線位置を固定したい伝票・ラベルはFixedDocumentです(4章)。
実装前に確認しておくこと
- 単位と座標系を先に決める。 画面のピクセル座標を印刷にそのまま流用しない。100分の1インチ単位や実寸(ミリ・インチ)を基準にする(2章)。
- 余白は2段構えだと理解する。
MarginBoundsを基準に描き、PageBoundsを基準にしない。Marginsを0にしてもハードマージンの内側にしか出せない(2章、図2)。 HasMorePagesを明示的に設定する。 「まだ描く行が残っているか」を判定してから設定する。既定値はfalseで、設定漏れが「2ページ目が出ない」の典型原因(3章)。- 印刷先プリンターを明示的に保持する。 既定プリンターに頼らず、設定ファイルやアプリの設定画面でプリンター名を持つ(7章)。
- GDIリソースは
usingで必ず解放する。Font・Brush・Penのリークは、長期稼働の本番環境でしか顕在化しません(7章)。 - サービスからの印刷は最初に境界を確認する。 セッション0ではダイアログを出せず、ユーザーに紐づくネットワークプリンターも見えません(7章)。
- 複数機種で検証する。 ハードマージンの大きさは機種で違うため、開発機で問題がなくても納品先で罫線が欠けることがあります(3章)。
関連記事
- Excel帳票出力の作り方 - COM/Open XML/テンプレート
- C#のExcel操作でEXCEL.EXEが残る問題 ── COM参照の解放パターンと置き換えの判断
- Windowsサービスの作り方と運用 ── タスクスケジューラとの使い分けからBackgroundServiceのサービス化まで
- Windowsのセッション分離をどう理解するか ── Session 0・RDP・複数ユーザー同時実行
- 産業用カメラ長期稼働クラッシュ調査 - ハンドルリーク編
関連する相談領域
合同会社小村ソフトでは、Windows業務アプリの印刷・帳票出力機能の設計、既存Excel資産を活かした帳票の作り直し、常駐サービスからの印刷・出力設計の技術相談を扱っています。
参考リンク
-
Microsoft Learn, PrintDocument Class.
PrintDocumentの役割(DocumentName・PrinterSettingsを設定しPrint()で印刷を開始する)と、WPFから印刷する場合はSystem.Printing名前空間を使うべきという案内について。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, PrintPageEventArgs.HasMorePages Property. 既定値が
falseであること、PrintPageイベントはこのプロパティがfalseになるまで繰り返し発生することについて。 ↩ ↩2 -
Microsoft Learn, PageSettings.PrinterResolution Property. ページの印刷解像度の既定値がプリンターの既定解像度であること、
PrinterSettings.PrinterResolutionsから選択可能な解像度一覧を取得できることについて。 ↩ ↩2 ↩3 -
Microsoft Learn, PageSettings.HardMarginX Property. ハードマージンがプリンターによって設定される物理的な余白を表すことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, PageSettings.Margins Property. ページの余白の既定値が上下左右1インチであること、
PrintPageイベントでBoundsプロパティと組み合わせて印刷可能領域を計算できることについて。 ↩ ↩2 ↩3 -
Microsoft Learn, Windows Print Path Overview. WindowsにGDI印刷パス(Win32アプリケーション由来)とXPS印刷パス(WPFアプリケーションまたはXPS Print API由来)の2つの主要な印刷パスが存在することについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Printing documents overview. WPFアプリケーションがXPS印刷パスを使うこと、
PrintDialogによる基本印刷、PrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriterによる高度な印刷、XPSDrv非対応プリンターへのGDI自動変換について。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Flow Document Overview.
FlowDocumentがウィンドウサイズや解像度などの実行時変数に応じてコンテンツを再レイアウトする、可読性優先の文書であることについて。 ↩ ↩2 -
Microsoft Learn, FixedDocument Class.
FixedDocumentが表示・印刷デバイスへの忠実な再現を優先するWYSIWYG設計であり、FlowDocumentとは設計思想が異なることについて。 ↩ ↩2 -
Microsoft Learn, PrintDialog Class.
System.Windows.Controls.PrintDialogがPrintTicketとPrintQueueをユーザー入力に応じて構成するダイアログであること、System.Windows.Forms.PrintDialogとは別クラスであることについて。 ↩ ↩2 -
Microsoft Learn, How to: Validate and Merge PrintTickets.
PrintQueue.GetPrintCapabilitiesでプリンターの対応機能を確認し、MergeAndValidatePrintTicketで要求内容をプリンター固有の妥当なPrintTicketにマージ・検証する手順について。 ↩ ↩2 -
Microsoft Learn, RemoveMPDW. Microsoft Print to PDFが既定でインストールされるオプション機能であり、印刷キューとドライバーパッケージという単位で構成・削除されることについて。 ↩ ↩2 ↩3
-
Microsoft Support, Considerations for server-side Automation of Office. Microsoftが、ASP/ASP.NET/DCOM/NTサービスを含む無人・非対話のクライアントアプリケーションからのOffice自動化を推奨・サポートしていないこと、Officeが対話的デスクトップとユーザープロファイルを前提に設計されていることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation. Windows Vista以降、セッション0がサービスや対話的ユーザーに関連しないアプリケーション専用に予約され、サービスがダイアログボックスを直接表示できなくなったことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, GDI Objects. GDIオブジェクトのハンドルがプロセスごとに既定の上限を持つこと、
GDIProcessHandleQuotaレジストリ値で256から65,536の範囲で上限を調整できることについて。 ↩ ↩2 -
Microsoft Learn, Introduction to printing. Windowsの印刷アーキテクチャがスプーラーとプリンタードライバーで構成されること、Win32 GDIアプリケーションの描画命令がEMFとしてスプールされる仕組みについて。 ↩ ↩2
-
Microsoft Learn, PageSettings.PrintableArea Property. プリンターが印字できる範囲を100分の1インチ単位で表すこと、このプロパティを使えばページの余白の外・印字可能な範囲の内側へ印刷できることについて。 ↩
-
Microsoft Learn, PrintDocument.OriginAtMargins Property. 既定値が
falseであること、falseのときは原点の決定に印字可能な範囲だけが使われPageSettings.Marginsが無視されることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
WinForms/WPFアプリの多言語化 ── resx・サテライトアセンブリ・カルチャ切り替えの実務
Windowsデスクトップアプリの多言語化を整理します。CurrentCultureとCurrentUICultureの違い、resxとサテライトアセンブリの仕組み、WPFでの現実的な方式選択、実行時の言語切り替え、書式・RTLまで解説します。
Windowsアプリのタスクトレイ常駐とトースト通知 ── NotifyIconの落とし穴とAppNotificationの選び方
業務Windowsアプリのタスクトレイ常駐とトースト通知の実装を整理します。NotifyIconの正しい使い方、Explorer再起動時の再登録、トーストAPI3種の選定判断表、通知が届かないケースまで解説します。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
CSVは「ただのテキスト」ではない ── C#業務アプリのCSV実務(文字コード・Excel互換・インジェクション対策)
業務アプリのCSV入出力の典型事故──Split(',')自作パース、BOMなしUTF-8の文字化け、先頭ゼロ落ち──を整理し、RFC 4180のルール、Shift_JISの扱い、TextFieldParserによる安全な読み込みまで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリの印刷・PDF出力機能の設計・実装はWindowsアプリ開発の相談範囲だからです。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- PrintDocumentとWPFの印刷、どちらで作るべきですか?
- 画面がWinFormsならPrintDocument、WPFならFlowDocument/FixedDocumentを使うのが素直です。技術を混在させる必要はなく、既存アプリの画面フレームワークに合わせるのが実務的な判断基準です。新規にWPFを選ぶなら、帳票が流し込み中心かレイアウト固定かでFlowDocumentとFixedDocumentのどちらを軸にするかを先に決めておくと迷いません。
- 帳票ライブラリは買うべきですか?
- 罫線の多い複雑な帳票、複数用紙サイズ対応、日本語フォントの埋め込みや縦書きなどを自前のGDI+/WPF描画で作り込むコストが大きい場合は、帳票ライブラリの導入コストの方が安くつくことが多いです。逆に、A4一覧表程度のシンプルな帳票であればPrintDocumentやFixedDocumentの自前実装で十分なことも多く、まずは要件の複雑さを見積もってから判断するのが安全です。
- PDFだけ作って印刷は使わない構成はありですか?
- 十分にありです。PDFはあくまで出力フォーマットの一つであり、印刷ダイアログや既定プリンターに依存しないPDFライブラリでの直接生成の方が、バッチ処理や自動化とは相性が良いことが多いです。Microsoft Print to PDFは対話的な操作を前提にしたドライバーであり、無人でのPDF生成には向きません。
- ラベルプリンターやレシートプリンターにも対応できますか?
- 対応可能ですが、汎用のPrintDocument/FixedDocumentの延長では対応しづらい場合があります。ラベルプリンターの多くは独自のコマンド言語やSDKを持ち、Windowsの標準印刷パスとは別経路で制御することも多いためです。まずは対象機種がWindowsの通常のプリンタードライバーとして振る舞うのか、専用SDK経由の制御が必要なのかを確認するところから始めてください。