Home |  バックナンバー |  ソースコード |  ニュース |  MSDN Magazine別冊 |
 MSDN Magazine 日本語版     

マイクロソフトテクノロジー エンサイクロペディア DVD-ROM版
  Back Issues Index
 2000
 2001
 2002
 2003
Source Code Index
MSDN Magazine 別冊
MSDN magazine US site
Microsoft.NETシリーズ 第4弾!
C#で始めるプログラミング「導入編」
C#で始めるプログラミング「導入編」
2002年10月18日発売!

Sample Code
 
C#で始めるプログラミング「オブジェクト指向編」
C#で始めるプログラミング「オブジェクト指向編」
2002年10月18日発売!

Sample Code
 
Microsoft.NETシリーズ 第3弾!
Microsoft.NET実践プログラミング - Best of MSDN Magazine 2001 -
Microsoft.NET実践プログラミング - Best of MSDN Magazine 2001 -
2002年7月2日発売!

Amazon.co.jpから御購入いただけます
 
Microsoft.NETシリーズ 第2弾!
Visual Basic.NETオブジェクト指向プログラミング入門
Visual Basic.NETオブジェクト指向プログラミング入門
2002年6月13日発売!

Amazon.co.jpから御購入いただけます
 
Microsoft.NETシリーズ 第1弾!
ASP.NETプログラマためのXMLテクニック
ASP.NETプログラマためのXMLテクニック
2002年3月1日発売!

Amazon.co.jpから御購入いただけます
 
msdn
March 2003 No.35 (2003年2月25日発売)
特集2 .NETの名前空間を探る
印 刷
.NET Printing名前空間を使ってWindowsフォームアプリケーションのプレビュー,印刷を実行する

Printing: Preview and Print from Your Windows Forms App with the .NET Printing Namespace
(February 2003 Vol.18 No.2)
Alex Calvo

Level :1 2 3
本記事は,C#とWindowsフォームについての知識があることを前提に書かれています。
SUMMARY:印刷は,完璧なWindowsアプリケーションすべてにとって密接不可分な機能である。しかし,Windowsアプリケーションに確固たる印刷機能を組み込むのは,退屈な作業であることが何度も示されてきた。しかし,.NET FrameworkのWindowsフォームからの印刷は,ドキュメント指向のアプローチを採用せざるを得ないことになり,結果としてクリーンで管理しやすいコードになる。System.Windows.Forms名前空間はすべての標準印刷ダイアログ(印刷プレビュー,ページ設定,印刷)の自然な統合を提供しているが,System.Drawing.Printing名前空間には,拡張,カスタマイズのためのさまざまなクラスが含まれている。本稿では,これらのクラスを取り上げ,それらが印刷機能に対するどのようにアクセスを用意しているかを説明する。ユーザーがほかの作業を続行できるようにするためのバックグラウンド印刷など,役に立つテクニックも紹介する。



 開発という視点から見たとき,Microsoft .NETはほとんどすべてのものを一新した。これらの変化のいくつか,特にWebフォームやADO.NETは,仕事の方法を根本的に変えてしまった。その一方で,System.Xmlのように,本質をより発展させ,既存のテクノロジーを単純に改良しただけという分野もある。Visual BasicやVisual C++を使っていた従来のプログラマにとって,Windowsフォームからの印刷は大幅な変更の部類に入る。しかし,.NET Frameworkの大半の部分と同様に,この変更は確かによい方向に向かっての変更である。

 Visual Basic PrintオブジェクトとPrintersコレクションの時代は遠く過ぎ去った。.NET Frameworkでは,一体型のPrintオブジェクトはなく,CurrentX,CurrentYプロパティを設定したり,EndDoc,NewPageのようなコマンドを発行することもない。Visual C++出身で.NETに来たプログラマは,印刷が退屈な仕事になり得ることをご存知だろう。たとえば,ページが正しく印刷されるようにするためには,Win32 APIを使って印刷過程を慎重に監視する必要がある。もうこれらの処理をしなくて済むようになったというわけではない。しかし,.NETを使えば,クリーンで管理しやすい印刷ロジックを組み立てられることになる。.NET Frameworkクラスは,印刷コードを完全にカプセル化することができる。一連の基底クラスから自分のコードを派生させればよいので,あらゆるタイプの追加機能がタダで手に入る。ほんの一例だが,.NETでは[印刷プレビュー]ダイアログのフックは簡単だ。

 本稿のほとんどのサンプルコードは,MSDN Magazine Webサイト(欄外参照)からダウンロードできる印刷サンプルアプリケーションから抜き出したものである。このサンプルWindowsフォームアプリケーション(Figure 1)は,.NETで印刷を行なうときに利用できるさまざまな新機能を具体的に示す。サンプルは,任意のテキストドキュメントを選択し,それを[印刷プレビュー]ダイアログや特定のプリンタに送ることができる。また,デモ用に,個々のページに透かし模様を表示すべきかどうかも指定できるようになっている。

Windowsフォームアプリケーション
Figure1:Windowsフォームアプリケーション

 [印刷プレビュー]ダイアログに出力するときには,アンチエリアスを有効にするかどうかを指定できる。アンチエリアスとは,画面上のテキストやグラフィックスをより滑らかに出力するための組み込み機能である。しかし,出力速度が遅くなるという代償があることを考えなければならない。また,[印刷プレビュー]ダイアログは,Windowsが提供するフォントを滑らかに表示するための機能(ClearType)を自動的に利用するので,アンチエリアスを使う必然性は下がる。サンプルアプリケーションは,出力をプリンタに送るときに,さまざまなオプションも選択できるようになっている。ステータスダイアログを表示するかどうか,ステータスバーにアニメのプリンタを表示するかどうか,バックグラウンドスレッドで印刷を行なうかどうかなどを指定できる。

 それでは,プリンタに出力を送るクイックアンドダーティな方法を出発点として,Windowsフォームの印刷について見ていくことにしよう。そのあとで,Windowsフォームから印刷するための正しい方法,すなわちPrintDocument派生のクラスを詳しく見ていく。

PrintDocumentクラスの使い方
 Windowsフォームからの印刷はドキュメント指向的でイベント駆動型の処理である。プログラマの仕事の大半は,汎用のPrintDocumentオブジェクトを使うことかPrintDcoument派生クラスを実装することになる。すぐあとで説明する理由から,PrintDocumentの継承のほうがよい道である。しかし,ときにはPrintDocumentクラス自体のインスタンスを使ったほうが手っ取り早いかもしれない。

 PrintDocument基底クラスを使って印刷するためには,PrintPageEventHandlerデリゲートと同じシグネチャを持つハンドラメソッド(静的メソッドでもインスタンスメソッドでもよい)にクラスのPrintPageイベントを結び付けなければならない。このイベントは,コードがPrintDocumentオブジェクトのインスタンスのPrintメソッドを呼び出したときに生成される。ページを実際に描くためには,PrintPageEventArgsオブジェクトのGraphicsプロパティを使わなければならない。PrintPageEventArgsクラスのインスタンスが,PrintPageイベントハンドラの引数として渡される。PrintPageEventArgsオブジェクトのGraphicsプロパティは,ページを描画するための描画サーフェスをカプセル化するGDI+オブジェクトを公開する(GDI+のいくつかの基本コマンドについては,本稿のなかで後述する)。複数ページを印刷するためには,PrintPageEventArgsオブジェクトのHasMorePagesプロパティを使って,印刷すべきものが残っていることを背後の印刷コントローラに通知する。HasMorePagesプロパティにtrueをセットすると,PrintPageイベントハンドラが再び呼び出されることが保証される。

 さらに,BeginPaint,EndPaint,QueryPageSettingsなどの一般的な印刷イベントに対するイベントハンドラもセットアップできる。BeginPaintは,PrintPageルーチンが使うオブジェクト(たとえばフォント)を初期化すべき場所として適している。QueryPageSettingsイベントは,1つ1つのPrintPageイベントが生成される直前に生成される。そこで,QueryPageSettingsEventArgs.PageSettingsプロパティを書き換えれば,ページごとに異なるページ設定を使って印刷することができる。文書全体のページ設定を変更するには,PrintDocumentクラスのDefaultPageSettingsプロパティを書き換えればよい。

 次に示すのは,PrintDocument基底クラスを使って印刷ジョブを開始する方法を示すコード例である。

PrintDocument printDoc = new PrintDocument();
printDoc.PrintPage += new PrintPageEventHandler(this.printDoc_PrintPage);
printDoc.Print();
    
// 印刷される個々のページに対してPrintPageイベントが生成される
private void printDoc_PrintPage(object sender, PrintPageEventArgs e)
{
    // TODO: e.Graphics GDI+オブジェクトを使ってページを印刷する
    // 残っているページがあるかどうかをPrintControllerに通知する
    e.HasMorePages = false;
}
コードからも分かるように,このアプローチには無数の欠点がある。最大の欠点は,複数のPrintPageイベントハンドラ呼び出しをまたがる形で状態情報付きのオブジェクトを管理しなければならないことである。たとえば,テキストドキュメントを印刷するときには,オープンされたStreamReaderオブジェクトを保持する必要がある。BeginPrintイベントでStreamReaderを初期化し,EndPrintイベントでクローズする。しかし,どうやったとしても,StreamReader変数(およびそのほかの変数)のスコープは,PrintPageイベントハンドラよりも広くなければならない。このようなことをすれば,印刷コードは丸見えとなり,コードのほかの部分から悪影響を受ける可能性がある。

PrintDocument派生クラスの実装
 Windowsフォームから印刷するときのよりよい方法は,汎用のPrintDocumentクラスの派生クラスを実装することである。そうすれば,たちまちカプセル化の恩恵が得られる。BeginPrint,EndPrint,PrintPageイベントにイベントハンドラを実装するのではなく,PrintDocument基底クラスのOnBeginPrint,OnEndPrint,OnPrintPageメソッドをオーバーライドする。OnPrintPageで使われる状態情報に対応したオブジェクトは,クラスの非公開フィールドという形で管理できるようになる。こうすれば,先ほど触れたようなコードの問題は完璧にシャットアウトできる。また,PrintDocument派生クラスには,カスタムプロパティ,メソッド,イベント,コンストラクタも自由に追加できる。

 サンプル印刷アプリケーションは,TextPrintDocumentという型のPrintDocument派生クラスを使う(Figure 2)。TextPrintDocumentクラスは,引数としてファイル名を取る多重定義コンストラクタを公開している。そのほかファイル名は,カスタムプロパティのFileToPrintで取得設定することもできる。存在しないファイルを設定すると,このプロパティは例外を発する。このクラスは,ページの背景としてグラフィックスを印刷するかどうかを指定する論理型の公開フィールド,Watermarkも持っている(背景グラフィックスは,Watermark.gifという名前の埋め込みリソースとしてアセンブリに格納されている)。最後に,TextPrintDocument派生クラスは,ページを展開するときに使うフォントを指定するFontプロパティも公開している。

 TextPrintDocumentクラスの核心は,OnPrintPageメソッドにある。OnPrintPageは,PrintPageEventArgsオブジェクトのGraphicsプロパティが提供するGDI+描画サーフェスを使ってページを描く部分である。PrintPageEventArgsオブジェクトには,このほかに,Cancel,HasMorePages,MarginBounds,PageBounds,PageSettingsプロパティを持っている。Cancelを使えば,印刷ジョブをキャンセルできる。MarginBoundsプロパティは,ページの印刷可能領域を表わすRectangleオブジェクトを返す。この矩形を使えば,各ページの印刷開始,終了位置が分かる。それに対し,PageBoundsは,余白を含むページ全体を表現する。

GDI+を使った印刷
 GDI+の詳細に触れようと思えば,それだけで1本の記事が必要になるだろう。そこで本稿では,TextPringDocumentクラスが個々のページをどのように展開するかという視点からGDI+を取り上げることにする。そして,一般的な印刷処理で使う可能性の高いいくつかのGDI+呼び出しを説明する。

 まず,OnPrintPageメソッドは,FontクラスのGetHeightメソッドを使って現在のフォントの高さを取得する。Graphicsオブジェクトを引数としてGetHeightメソッドを呼び出せば,現在のページのdpi(インチ当たりのドット数)を取得することができる。現在のフォントの高さが分かったら,現在のMargineBounds.Heightを使ってページ当たりの行数を計算する。次に,テキストファイルを1行ずつ読み込み,DrawStringメソッドを使って印刷する。ファイルの末尾に達する前にページの末尾に達したら,HasMorePagesプロパティにtrueをセットする。

 以上からも分かるように,基本的な印刷は,DrawStringを使うだけで実現できる。しかし,GDI+は,それぞれいくつもの多重定義を持つ15種類以上の描画メソッドを持つ。描画には,ベクタグラフィックス(DrawBezier,DrawEllipseなど)とラスタグラフィックス(DrawImage,DrawIconなど)の両方を使える。OnPrintPageメソッドがDrawImageを使って透かし模様を描画することに注意していただきたい。クリッピングや変形などたくさんのGDI+の高度な機能を利用することもできる。Visual Basic Printオブジェクトで同じことができるだろうか。

PrintControllerクラス
 先ほど,サンプル印刷アプリケーション(Figure 1)がステータスダイアログ,ステータスバーのアニメアイコン(印刷中にページを吐き出す内容のもの)をオプションで表示できると言った。これらの機能は,ともにPrintController派生クラスを使って実装される。PrintControllerは,.NET Framework内のStandardPrintController,PrintControllerWithStatusDialog,PreviewPrintControllerの3つの具象クラスにより実装される抽象クラスである。

 PrintControllerは,PrintDocumentがどのように印刷するかを決める。PrintDocumentクラスは,自らの基礎となっているPrintControllerをプロパティとして外部に公開している。PrintDocumentのPrintメソッドを呼び出すと,背後のPrintControllerのOnStartPrint,OnEndPrint,OnStartPage,OnEndPageメソッドが呼び出される。Figure 3は,PrintDocumentとPrintControllerの間で発生する一連のイベントをまとめたものである。値を返すのはOnStartPageメソッドだけで,戻り値はGraphics型である。すでにお分かりのように,Graphicsは,PrintPageEventArgs引数を介してPrintDocumentに渡されるGDI+描画サーフィスである。

印刷の流れ
Figure3:印刷の流れ

 デフォルトのPrintControllerは,PrintControllerWithStatusDialog型だが,印刷ステータスダイアログをオフにしたいときには,StandardPrintControllerを使わなければならない。PreviewPrintControllerは,PrintPreviewDialog,PrintPreviewControlクラスによって使われる。PrintControllerWithStatusDialogクラスはSystem.Windows.Forms名前空間に含まれているが,StandardPrintControllerとPreviewPrintControllerはSystem.Drawing.Printing名前空間に含まれている。PrintControllerWithStatusDialogは,引数として別のPrintControllerを取る多重定義コンストラクタを提供している。これを利用すれば,PrintControllerWithStatusDialogに独自コントローラの追加機能を結合できる。サンプル印刷アプリケーションを実行するときに,PrintControllerWithStatusDialogとPrintControllerWithStatusBarの2つのチェックボックスをともにチェックしてみていただきたい。このときのコードは,次のようになる。

CustomPrintDocument printDoc = new CustomPrintDocument();
CustomPrintController printCtl = new CustomPrintController();
printDoc.PrintController =
    new PrintControllerWithStatusDialog(printCtl, "Dialog Caption");
printDoc.Print();
 サンプル印刷アプリケーションは,PrintControllerWithStatusBar型のカスタムプリントコントローラを使っている(Figure 4)。PrintControllerWithStatusBarは,アニメのプリンタアイコンを表示すべきステータスバーパネルを指定するStatusBarPanelプロパティを公開している。アニメの実行のためには,System.Timers.Timer型のタイマを使った。System.Timers.Timerクラスは,バックグラウンド印刷を行なうようなマルチスレッドアプリケーションでも快適に動作する。

印刷ダイアログの使い方
 .NETでの印刷の優れたところは,PrintDocumentと印刷ダイアログがしっくり噛み合っているところである。System.Windows.Forms名前空間には,PrintDialog,PrintPreviewDialog,PageSetupDialogの3種類の印刷ダイアログが含まれている。また,UIの設計にさらに幅を持たせてくれるダイアログなしのPrintPreviewControlクラスもある。サンプル印刷アプリケーションは,単純にするために,PrintPreviewDialogクラスを使っている(Figure 5)。

[印刷プレビュー]ダイアログ
Figure5:[印刷プレビュー]ダイアログ

 PrintDialogクラスは,Windowsの標準[印刷]ダイアログを表示し,プリンタを選択したり,印刷するページや印刷部数を指定したりするのに使用することができる。このクラスは,次のように簡単に使える。

CustomPrintDocument printDoc = new CustomPrintDocument();
PrintDialog dlgPrint = new PrintDialog();
    
dlgPrint.Document = printDoc;
    
if (dlgPrint.ShowDialog() == DialogResult.OK)
{
    printDoc.Print();
}
 PrintPreviewDialogクラスはもっと簡単に使える。[印刷プレビュー]ダイアログは,開発中には特に役に立つ。PrintDocumentのデバッグで紙を大量に節約できるのである。このダイアログは,次のようにして使う。

CustomPrintDocument printDoc = new CustomPrintDocument();
PrintPreviewDialog dlgPrintPreview = new PrintPreviewDialog();
// ここでdlgPrintPreviewのオプションプロパティを設定する
dlgPrintPreview.Document = printDoc;
dlgPrintPreview.ShowDialog();
PrintPreviewDialogクラスのインスタンスを作成したら,そのDocumentプロパティにPrintDocument派生クラスの任意のインスタンスをセットする。すると,PrintPreviewClassのShowDialogメソッドが,ドキュメントのプレビュー表示を自動的に行なってくれる(.$fig 5 pic )。これ以上簡単にすることはできないだろう。

 PageSetupDialogクラスも同じように動作する。しかし,Documentプロパティのサポート以外に,PageSettings,PrinterSettingsプロパティを選ぶことができ,そこにPageSettings,PrinterSettingsクラスのインスタンスをセットすることができる。PageSettingsクラスは,Margins,PaperSizeなどの実際の印刷ページに適用する設定を定義する。一方,PrinterSettingsクラスはFrontPage,PrinterNameなど,どのようにドキュメントを印刷するかという情報を指定する。またPrinterSettingsクラスは,静的メソッドのInstalledPrintersによって,利用できるプリンタのリストを取得することができる。このメソッドは,PrinterSettings.StringCollectionオブジェクトを返す。[ページ設定]ダイアログが制御を返すとき,Document,PageSettings,PrinterSettingsオブジェクトは,ユーザーの指定に従って書き換えられる。

バックグラウンド印刷
 スレッド管理は難しい仕事であり,マルチスレッドアプリケーションを書こうとすると問題が起きやすいことを否定するつもりはない。しかし,バックグラウンドスレッドを使えば,印刷時の使い勝手がよくなることは間違いない。バックグラウンドで印刷ジョブが処理されている間も,ユーザーはアプリケーションの印刷以外の機能を使い続けることができる。バックグラウンド印刷を試すには,大きなドキュメントを使って,プリンタを一時停止させるとよい。紙を浪費する必要はない。またThreadクラスのSleep静的メソッドを使えば,印刷速度を下げることができる。サンプル印刷アプリケーションは,出力をプリンタに送るときに,これを試せるようになっている。[印刷プレビュー]ダイアログでバックグラウンド印刷をしても意味はない。要約すれば,バックグラウンド印刷は,次のような仕組みである。

private void cmdBackgroundPrint_Click(object sender, System.EventArgs e)
{
    Thread t = new Thread(new ThreadStart(PrintInBackground));
    t.IsBackground = true;
    t.Start();
}
private void PrintInBackground()
{
    printDoc.Print();
}
 デリゲートを使って同じことを実現することもできる。サンプル印刷アプリケーションを初めて作ったとき,私はThreadクラスを使っていた。しかし,印刷が完了したときに通知が欲しいと思ったので,デリゲートを使うように書き換えた。デリゲートのBeginInvokeメソッドを使えば,非同期処理が完了したときに通知を受け付けるコールバック関数を指定できる。マルチスレッド環境で印刷完了時に「Print」ボタンを再び有効にするためには,このテクニックが必要である。

 Figure 6には,サンプル印刷アプリケーションを動かすためのコードの大半が含まれている。特に強調したいのは,PrintInBackgroundDelegateというデリゲートが含まれていることである。「Background thread」チェックボックスがチェックされているときには,「Print」ボタンのClickイベントハンドラのなかで,デリゲートのBeginInvokeメソッドが呼び出される。チェックされていなければ,メインUIスレッドから直接PrintDocumentのPrintメソッドが呼び出される。

 コーディング上,問題を起こすのは,PrintDocumentが依存する状態情報に対応したオブジェクトに関連することである。たとえば,先ほどの例で言えば,ユーザーがcmdBackgroundPrintを複数回クリックしたらどうなるだろうか。予測不能である。PrintDocument.PrintPageイベントは,状態情報に対応したフィールドを使うことが多いので,このような場合には,非常に面倒なことになることがある。この種の問題に対処するための簡単な方法は,単純に一部のUIコントロールを無効にして,ユーザーが同じコマンドを再発行できないようにすることである。その一方で,アプリケーションのほかの機能は使えるままにしておかなければならない。サンプル印刷アプリケーションは,このアプローチを使っている。なお,バックグラウンド印刷にチャレンジするときには,まず本号のIan Griffithの記事を読まれることをお奨めする。

まとめ
 本稿は,Windowsフォームの.NETネイティブな印刷方法を紹介したが,.NETには,CrystalReportなど,その他のプリンタ出力方法もある。ネイティブな印刷機能よりもレポートツールのほうが適切な場合は確かにある。しかし,既製品のオーバーヘッドを嫌い,多くのWindows用デスクトップアプリケーションが持つような,カスタムまたは専用の印刷機能を必要とする場合には,間違いなくネイティブ印刷を使うべきである。


Alex Calvo:Alex Calvoは,Microsoft .NETのMCSD,MCADで,.NET,Visual Basic,COM,SQL Serverを専門とするコネチカット州のコンサルティング企業,Developer Box LLC(http://www.developerbox.com)の共同設立者である。連絡先は,acalvo@hotmail.com。



グレープシティ(株)社長インタビュー
 back to top Last Updated: 2004/07/23     
Copyright (C) 2001 ASCII Corporation. All rights reserved.
プライバシーポリシー