WinUI 2(C++/WinRT)で独自の印刷 UI を作ると、「プリンタ HDC を複数ドキュメントで使い回してよいか」「どのタイミングで破棄すべきか」「長時間保持や別スレッド再利用は安全か」が必ず論点になります。本稿はその疑問に対し、GDI の設計と実運用を踏まえた“壊れない印刷ワークフロー”を、サンプルコードと設計指針で徹底解説します。
結論と要点:HDC はドキュメント(印刷ジョブ)単位で作成・破棄する
結論はシンプルです。プリンタ HDC(デバイスコンテキスト)は、1 ドキュメント=1 ジョブの単位で CreateDC し、StartDoc → StartPage … → EndPage → EndDoc を終えたら 必ず DeleteDC で破棄します。ジョブをまたいだ再利用や長時間の保持、スレッドをまたいだ共有は推奨されません。再利用が“たまたま動く”ケースはありますが、ドライバ依存・スプーラ状態依存であり、信頼性・保守性・再現性のいずれも低下します。
なぜ再利用しないのか:GDI とスプーラの性質を理解する
プリンタ HDC は GDI リソースであり、内部的にドライバやスプーラ(印刷キュー)と結び付いた状態を持ちます。ジョブ境界を越えて HDC を握り続けると、プリンタ設定変更・ネットワーク切断・ドライバ再読み込みなどの外的変化に追従できず、ResetDC でも回復できない不整合が発生しやすくなります。特に近年の Windows は XPS/EMF 混在環境や仮想プリンタ、クラウドプリントなど、ドライバ側の抽象化が進み、“同じ HDC が次のジョブでも同じ意味を持つ”保証が弱い点に注意が必要です。
| リスク | HDC をジョブ間で再利用した場合の症状 | 推奨対策 |
|---|---|---|
| 設定不一致 | 用紙サイズ・向き・両面の変更が次ジョブに反映されない/誤った描画座標になる | 毎ジョブ CreateDC → 最新 DEVMODE を反映 → DeleteDC |
| ドライバ再初期化 | スリープ復帰・ネットワーク再接続後に StartDoc が失敗/印字が欠落 | ジョブ開始時に新規 HDC を確保してドライバと状態を同期 |
| スレッド安全性 | 別スレッドから同一 HDC を触って不定動作・断続的な GDI エラー | スレッドごとに HDC を作成し共有しない |
| リソース枯渇 | 未解放 HDC の積み上がりで GDI リソース不足 | ジョブ完了ごとに必ず DeleteDC |
正しいライフサイクル:最小構成のテンプレート
// 1 ドキュメント(1 印刷ジョブ)を印刷する最小パターン
HDC hdc = CreateDCW(L"WINSPOOL", printerName.c_str(), nullptr, pDevMode); // pDevMode は最新設定
DOCINFOW di{};
di.cbSize = sizeof(DOCINFOW);
di.lpszDocName = L"MyDocument";
if (hdc) {
if (StartDocW(hdc, &di) > 0) {
if (StartPage(hdc) > 0) {
// --- ここに GDI 描画処理 ---
// TextOutW(hdc, x, y, L"Hello", 5);
// BitBlt(...); Rectangle(...); etc.
EndPage(hdc);
}
EndDoc(hdc);
}
DeleteDC(hdc); // ★ジョブ終了後に必ず破棄
}
複数部数・連続印刷でも、ループ内で都度 CreateDC → DeleteDC すれば十分です。CreateDC のオーバーヘッドは、描画・スプールのコストに比べて小さいのが実測の通例です。
WinUI 2(C++/WinRT)との統合設計:責務の分離
モダン UI と印刷実装を混在させると複雑化します。以下のように責務を分けると安全です。
- UI 層(WinUI 2): プリンタ選択、用紙・向き・両面・部数などの論理設定を収集。
- 印刷オーケストレータ(バックグラウンドスレッド 1 本): 設定から
DEVMODEを生成し、各ジョブにつき HDC を生成→印刷→破棄。 - 描画関数(純粋関数化):
HDCとページ番号を受け取り、同一描画が常に再現できるように外部状態に依存しない。
DEVMODE の取り扱い:DocumentProperties で“最新設定”を毎回反映
印刷ジョブ開始時には、DocumentProperties により最新のプリンタ設定(DEVMODE)を取得・編集して HDC 作成に渡します。ユーザーの UI 選択は DEVMODE にマージして使うのが確実です。
// 例:DEVMODE を取得・編集して CreateDC に渡す
HANDLE hPrinter = nullptr;
if (OpenPrinterW(const_cast<LPWSTR>(printerName.c_str()), &hPrinter, nullptr)) {
LONG size = DocumentPropertiesW(nullptr, hPrinter, const_cast<LPWSTR>(printerName.c_str()),
nullptr, nullptr, 0);
std::vector<BYTE> buf(size);
PDEVMODEW pDev = reinterpret_cast<PDEVMODEW>(buf.data());
if (DocumentPropertiesW(nullptr, hPrinter, const_cast<LPWSTR>(printerName.c_str()),
pDev, nullptr, DM_OUT_BUFFER) == IDOK) {
// --- UI の選択を反映 ---
pDev->dmFields |= DM_ORIENTATION | DM_DUPLEX | DM_COLOR;
pDev->dmOrientation = DMORIENT_PORTRAIT; // or DMORIENT_LANDSCAPE
pDev->dmDuplex = DMDUP_SIMPLEX; // or DMDUP_VERTICAL/ HORIZONTAL
pDev->dmColor = DMCOLOR_COLOR; // or DMCOLOR_MONOCHROME
HDC hdc = CreateDCW(L"WINSPOOL", printerName.c_str(), nullptr, pDev);
// ... StartDoc/StartPage/EndPage/EndDoc/ DeleteDC ...
}
ClosePrinter(hPrinter);
}
DEVMODE を都度取得して HDC に渡すことで、プリンタの既定設定の変更・別アプリによる設定変更も漏れなく反映できます。
スレッド設計:HDC はスレッドローカル、描画はシリアライズ
GDI は同一オブジェクトの並行アクセスに強くありません。印刷スレッドを 1 本用意し、そのスレッド内で HDC の生成~破棄を完結させましょう。UI スレッドからは印刷ジョブ情報のみをキューイングします。
// 疑似コード:印刷ワーカー
struct PrintJob { std::wstring printer; /* devmode, pages, payload */ };
concurrency::unbounded_buffer g_queue;
std::atomic_bool g_stop{false};
void PrintWorker() {
while (!g_stop) {
PrintJob job;
if (try_receive(g_queue, job)) {
// ここで CreateDC -> StartDoc -> ... -> DeleteDC
// HDC はこのスレッドでのみ使用
} else {
Sleep(10);
}
}
}
別スレッド再利用は避け、スレッドごとの新規 HDCとしてください。ジョブ間で HDC を共有すると、断続的な GDI エラーやスプールの詰まりの原因となります。
キャンセルと堅牢性:SetAbortProc とエラーパス
ユーザーキャンセルやドライバエラーに備えて、SetAbortProc を設定し、失敗時は AbortDoc を必ず呼びます。
static int CALLBACK AbortProc(HDC, int) {
// 外部フラグを見てユーザーキャンセルを反映
return /* continue ? */ TRUE : FALSE;
}
bool PrintOneDocument(HDC hdc, const DOCINFOW& di, int pageCount) {
SetAbortProc(hdc, AbortProc);
if (StartDocW(hdc, &di) <= 0) return false;
for (int i = 0; i < pageCount; ++i) {
if (StartPage(hdc) <= 0) { AbortDoc(hdc); return false; }
// 描画...
if (EndPage(hdc) <= 0) { AbortDoc(hdc); return false; }
}
if (EndDoc(hdc) <= 0) { AbortDoc(hdc); return false; }
return true;
}
この構成により、ジョブ単位で“始まりと終わり”が明確になり、障害時の切り離しが容易になります。
HDC 再利用を避けるメリット(再確認)
| 観点 | ジョブごとに HDC を作成・破棄 |
|---|---|
| 安定性 | ドライバ・スプーラの最新状態を毎回同期。外部変化に強い。 |
| セキュリティ | ハンドルの長期保持を避け、ハンドル流出・二重解放のリスク低減。 |
| 保守性 | ジョブ境界で責務を閉じるため、コードが読みやすい・デバッグしやすい。 |
| 可搬性 | プリンタやドライバの違いに左右されにくい。 |
| 可観測性 | ログやメトリクスをジョブ単位で収集・可視化しやすい。 |
それでも再利用したい? ― “自己責任モード”の注意点
どうしても HDC を再利用したい場合は、以下を守ってもなお“不定”が残る点を理解してください。
- 同一スレッドで扱う(他スレッド不可)。
EndDoc直後にResetDCを呼ぶ(ドライバ依存で完全初期化されない場合あり)。- プリンタ設定(
DEVMODE)は一切変更しない(用紙・向き・解像度など)。 - UI からのジョブ混在防止:直列化(1 つずつ確実に完了させる)。
これらを守っても、ドライバ実装差やネットワーク事情で不具合が残る可能性があり、サポートコストは確実に増大します。
描画スケーリングと座標:DPI を正しく扱う
プリンタごとに LOGPIXELSX / LOGPIXELSY が変わります。絶対寸法で描画したい場合は mm → デバイス単位の変換関数を用意しましょう。
struct Dpi { int x, y; };
inline Dpi GetDpi(HDC hdc) {
return { GetDeviceCaps(hdc, LOGPIXELSX), GetDeviceCaps(hdc, LOGPIXELSY) };
}
inline int mm_to_dx(HDC hdc, double mm) {
auto dpi = GetDpi(hdc);
return static_cast(mm * dpi.x / 25.4);
}
inline int mm_to_dy(HDC hdc, double mm) {
auto dpi = GetDpi(hdc);
return static_cast(mm * dpi.y / 25.4);
}
また、GetDeviceCaps(hdc, HORZRES)/VERTRES でページ描画領域を取得し、余白・センタリング・拡大縮小などを一元管理するとレイアウトの破綻を防げます。
EMF による前処理と再描画コスト削減
同一コンテンツを複数部印刷する、あるいは異なるプリンタに同じ内容を出したい場合は、EMF(拡張メタファイル)を活用すると効率的です。EMF には GDI の描画コマンドが記録されるため、HDC は毎ジョブ新規作成しつつ、描画コストはメタファイルの再生(PlayEnhMetaFile)で最小化できます。
// 1) EMF を作成
HDC ref = CreateDCW(L"DISPLAY", nullptr, nullptr, nullptr);
HENHMETAFILE hemf = nullptr;
{
RECT r = {0,0, 2480, 3508}; // A4 @ 300dpi の概形など
HDC meh = CreateEnhMetaFileW(ref, nullptr, &r, L"MyDoc\0\0");
// --- ここで meh に対して描画 ---
hemf = CloseEnhMetaFile(meh);
}
DeleteDC(ref);
// 2) 印刷時:各ジョブで新規 HDC に再生
HDC phdc = CreateDCW(L"WINSPOOL", printer.c_str(), nullptr, pDev);
StartDocW(phdc, &di);
StartPage(phdc);
PlayEnhMetaFile(phdc, hemf, &(RECT{0,0, GetDeviceCaps(phdc,HORZRES), GetDeviceCaps(phdc,VERTRES)}));
EndPage(phdc);
EndDoc(phdc);
DeleteDC(phdc);
// 3) すべての印刷が終わったら EMF を破棄
DeleteEnhMetaFile(hemf);
この方式なら、HDC の再利用不要・描画の再実装負担も軽いというバランスが取れます。
UI からのプリンタ選択と既定プリンタの扱い
独自 UI でプリンタを列挙するには EnumPrinters を用います。既定プリンタは GetDefaultPrinter で得られます。
// 既定プリンタ名の取得
DWORD needed = 0;
GetDefaultPrinterW(nullptr, &needed);
std::wstring def(needed, L'\0');
if (GetDefaultPrinterW(def.data(), &needed)) {
def.resize(wcslen(def.c_str()));
// def が既定プリンタ名
}
プリンタ切り替え時は、新しい DEVMODE を必ず取得し、以降のジョブは新規 HDC を使いましょう。
RAII で“閉じ忘れ”をなくす:HDC の安全な所有
例外や早期リターンでも確実に DeleteDC されるよう、RAII を採用します。
struct HdcDeleter { void operator()(HDC h) const noexcept { if (h) DeleteDC(h); } };
using unique_hdc = std::unique_ptr<std::remove_pointer_t<HDC>, HdcDeleter>;
unique_hdc make_printer_hdc(const std::wstring& printer, PDEVMODEW dev) {
return unique_hdc(CreateDCW(L"WINSPOOL", printer.c_str(), nullptr, dev));
}
これにより、return のパスが増えてもリークしません。HBITMAP や HFONT など他の GDI オブジェクトも同様に RAII 化しましょう。
エラーハンドリングの実務:ログ粒度と復旧戦略
印刷は環境依存要素が多く、“ユーザーの環境でだけ再現する”問題が起きがちです。次の粒度でログを残すと、現地調査なしでも原因を絞り込めます。
- ジョブ境界:プリンタ名、ドライバ名、
DEVMODEの主要フィールド(用紙、向き、解像度、両面)。 - API 呼び出し結果:
StartDoc/StartPage/EndPage/EndDocの戻り値とGetLastError()。 - スプール進捗:ページ番号、レンダリング時間、バイト数(概算)。
- 例外・キャンセル:ユーザーキャンセルの時刻、
AbortDoc実施の有無。
復旧戦略としては、失敗したら同一設定で HDC を新規作成して再試行、それでもダメならプリンタを既定に切り替える提案、PDF へのフォールバック出力(後印刷)など段階的に。「HDC の再利用」は復旧策として選ばないでください。
パフォーマンス最適化:何を“キャッシュ”すべきか
HDC 自体はキャッシュしません。代わりに、以下の“使い回しに耐える”データをキャッシュします。
| キャッシュ対象 | 効果 | 注意点 |
|---|---|---|
| レイアウト情報(段組・余白・座標) | 各ページの描画計算を省力化 | 用紙サイズ変更時は再計算 |
| フォント/ビットマップのロード済みデータ | ディスク I/O 削減 | 実 GDI オブジェクト(HFONT/HBITMAP)は HDC に結び付けず都度選択 |
| EMF(前述) | 同内容の再描画を高速化 | 最終的なプリンタ解像度に合わせてスケール |
| 印刷設定プリセット | UI 操作時間の短縮 | プリンタ変更時は DEVMODE の整合性を再検証 |
チェックリスト:信頼性のための 12 の約束
- 各ジョブで
CreateDC→DeleteDCを徹底。 DEVMODEはDocumentPropertiesで必ず最新取得。- スレッド間で HDC を共有しない。
- 失敗時は
AbortDoc、キャンセルはSetAbortProc。 - ページごとに
StartPage/EndPageを対応づける。 - ジョブは直列化し、競合を避ける。
- ログ粒度をジョブ・ページ・API で分ける。
- EMF やレイアウトのキャッシュでレンダリング負荷を下げる。
- プリンタ切替時は HDC を作り直す。
- ハンドルは RAII で管理、漏れをなくす。
- デバイス座標は DPI に基づき計算する。
- エラー時の再試行は「新規 HDC で」行う。
サンプル:WinUI 2 のボタンから安全に印刷を起動
「印刷」ボタン押下で印刷ジョブを作る最小例です。UI スレッドはキュー投入だけ、実印刷はワーカーに任せます。
// Pseudo (C++/WinRT with Win32 interop)
void MainPage::OnPrintButtonClick(IInspectable const&, RoutedEventArgs const&)
{
PrintJob job{};
job.printer = selectedPrinterName; // UI で選択
job.devmode = BuildDevModeFromUi(); // 上述の DocumentProperties ベース
send(g_queue, std::move(job)); // ワーカースレッドへ
}
// ワーカー側(1 本)
void ProcessJob(PrintJob& job)
{
DOCINFOW di{ sizeof(DOCINFOW) };
di.lpszDocName = L"WinUI2 Sample";
auto hdc = make_printer_hdc(job.printer, job.devmode.get());
if (!hdc) return;
SetAbortProc(hdc.get(), AbortProc);
if (StartDocW(hdc.get(), &di) <= 0) return;
for (int page = 0; page < job.pageCount; ++page) {
if (StartPage(hdc.get()) <= 0) { AbortDoc(hdc.get()); return; }
DrawPage(hdc.get(), page, job); // 純粋描画関数
if (EndPage(hdc.get()) <= 0) { AbortDoc(hdc.get()); return; }
}
if (EndDoc(hdc.get()) <= 0) { AbortDoc(hdc.get()); }
}
この構造にしておけば、UI は“何部印刷するか・どのプリンタか”という論理情報だけに集中でき、GDI の複雑さをアプリ全体に拡散させずに済みます。
トラブルシューティング:症状から逆引き
| 症状 | 主な原因 | 対処 |
|---|---|---|
StartDoc が 0 以下 | プリンタに到達できない/権限不足/DEVMODE 不整合 | プリンタ名と接続状態を再取得、DEVMODE を再生成、新規 HDC で再試行 |
| 数ジョブ後に印字が乱れる | HDC を使い回して内部状態が腐る | ジョブごとに HDC を作成・破棄に改める |
| 両面や用紙が反映されない | DEVMODE を更新せずに再利用 | 毎回 DocumentProperties で最新を取得・マージ |
| 別スレッドで稀に固まる | HDC のスレッド越え共有 | 印刷ワーカーを 1 本化。HDC はスレッドローカル。 |
| 印字位置がずれる | DPI や印刷可能領域の誤算 | LOGPIXELSX/Y と HORZRES/VERTRES を使い直す |
設計判断の指針:HDC 再利用を“しない”ことで得られる最適化余地
HDC を切り替える設計は、一見コストに見えても、ジョブごとに“初期化パック”を必ず通るため、状態のねじれがなくなります。結果として:
- バグ報告の再現率が上がり、修正が速い。
- ロールバックや回避策(PDF フォールバック等)が組み込みやすい。
- 機能追加(別プリンタ・別用紙)の影響半径が小さい。
パフォーマンスは EMF やレイアウトキャッシュで補い、安定性と開発速度のトレードオフを最良点に置くのが、印刷機能における現実解です。
セキュリティと権限:ハンドルの寿命を短く保つ
ハンドルはプロセスの攻撃面になり得ます。長寿命ハンドルはデバッグも難しく、障害時にぶら下がりやすい。短く持って確実に解放するのが基本です。印刷はユーザーの機密文書を扱う可能性がある以上、ハンドルの生存時間=露出時間を最小にする設計が望ましいと言えるでしょう。
実運用のヒント:テスト観点と自動化
- プリンタ多様性:ローカル USB、ネットワーク共有、仮想 PDF、業務用複合機の 4 系統で最低限検証。
- 設定組み合わせ:用紙(A4/Letter)、向き(縦/横)、両面(単/長辺/短辺)、カラーモード(カラー/モノクロ)。
- スリープ/ネットワーク揺らぎ:印刷途中にスリープ→復帰、Wi-Fi 再接続。
- 大量ジョブ:100 ジョブを連続投入し、リークと詰まりを監視。
- キャンセル:ページ 1/中盤/最終ページでのキャンセル挙動。
CI には実機プリンタは難しいものの、EMF 出力の静的比較や、CreateDC → StartDoc → … の API 成功率、ハンドル数の推移など、自動化できる観点は意外に多いです。
まとめ
プリンタ HDC の再利用は公式に推奨されず、各ドキュメントで取得・破棄するのが最適解です。ジョブ境界を明確にし、DEVMODE を毎回見直し、HDC はスレッドローカルに限定する。パフォーマンスは EMF・レイアウト・アセットのキャッシュで補い、信頼性は SetAbortProc と堅牢なエラーハンドリングで担保する。これこそが、WinUI 2(C++/WinRT)で壊れない印刷機能を提供するための実践的ベストプラクティスです。

コメント