S-JIS[2002-11-03]

印刷・印刷プレビュー

AppWizardとデバイスコンテキストのおかげで印刷は非常に簡単に実装できますが、 ページに関するやりくりは少々コツが要ります。


単純な印刷

まずは、AppWizardでプロジェクトを作成します。このときに、「アプリケーションに組み込む機能」で 「印刷および印刷プレビュー」にチェックを入れます。 これだけで、印刷メニューや印刷を準備する関数がCViewの派生クラスに用意されます。

testView.cpp:
/////////////////////////////////////////////////////////////////////////////
// CTestView クラスの描画

void CTestView::OnDraw(CDC* pDC)
{
	CTestDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);

	// TODO: この場所にネイティブ データ用の描画コードを追加します。
	pDC->SetMapMode(MM_HIMETRIC);

	//赤い箱
	pDC->FillSolidRect(2000,-1000,3000,-1000,RGB(255,0,0));

	//黒い線
	pDC->MoveTo(2000,-1000);
	pDC->LineTo(5000,-2000);

	//青い字
	pDC->SetTextColor(RGB(0,0,255));
	pDC->SetBkColor(RGB(255,255,255));
	pDC->TextOut(2000,-2000,"テスト");
}

まずは画面に表示するところからというわけで、箱と線と字を表示してみました。

MM_HIMETRICというのは、pDCで描く座標系を0.01ミリメートル単位に 指定するものです。画面のみならドット単位で考える方が簡単ですが、印刷するときに 実際の用紙のサイズが限られている事を考えると、ミリメートルの方が計りやすいでしょう。
ただしMM_HIMETRICを指定すると、左上の座標は(0,0)で X方向の正の数値は右向きですが、 Y方向の正の数値は上向きで 普通とは逆になります。それで、上記のプログラムを見て分かるとおり、 Y座標には負の数値を使っています。
ちなみに0.01mm単位なので、1000は1cmとなります。 このサンプルの赤い箱は3cm×1cm、1文字はおおよそ5mmですね。

もちろん、ただ普通に画面に表示したものをそのまま印刷したいだけなら、MM_HIMETRICを 使う必要はありません。


AppWizardで印刷及び印刷プレビューを指定したので、このまま何もコーディングしないで 印刷プレビューを表示させることが出来ます。
メニューバーの「ファイル」→「印刷プレビュー」を選んで見てみましょう。
ちなみに印刷プレビュー用のツールバーのボタンは(少なくともVC++4.0Standardには)ありませんが ID_FILE_PRINT_PREVIEWのIDでツールバーにボタンを作ってやれば、すぐ出来ます。

なんか字の表示が変ですね(汗)
これは、僕が使っているVC++4.0(Standard SP無し)のバグだと思います。 紙に印刷すれば、ちゃんと表示されてます。


さて、なぜ画面用のコーディングしかしていないのに印刷できるか?ということですが、 これがMFCとデバイスコンテキストのおかげです。
画面と印刷のそれぞれについて どういう動きをしているかというと、

画面描画 印刷プレビュー 印刷
画面描画イベント プレビューイベント 印刷イベント
CView::OnPaint 画面用DCを準備 CPreviewView::OnDrawプレビュー用DCを準備 CView::OnFilePrint 印刷用DCを準備
CTestView::OnDrawDC(画面用)に描画 CTestView::OnDrawDC(プレビュー用)に描画 CTestView::OnDrawDC(印刷用)に描画

細かい部分は省略していますが、最終的にはどれもCTestView::OnDrawが呼ばれます。 ただし、その時に使われるデバイスコンテキストが異なるので、最終的に描画される場所も 画面だったりプリンターだったりするのです。


用紙サイズ

用紙サイズはメニューバーの「ファイル」→「プリンターの設定」で変えることが出来ますが、 プログラムの中で指定することも出来ます。

testView.cpp:
void CTestView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
	// TODO: 印刷前の特別な初期化処理を追加してください。
	DEVMODE *dp=pInfo->m_pPD->GetDevMode();
	dp->dmPaperSize  =DMPAPER_A4;        	//A4用紙
	dp->dmOrientation=DMORIENT_LANDSCAPE;	//横向き
	pDC->ResetDC(dp);
}

OnBeginPrintingの引数のpDCとpInfoは、最初はコメントアウトされているので注意!

用紙に指定できる定数等については、DEVMODEのヘルプに載っています。


複数ページ印刷

データが複数ページにわたることを想定して1ページずつ画面に表示するような構造にしている場合、 複数ページまとめて印刷したり、現在のページのみ印刷したりしたいですよね。
そういったデータ構造は、ドキュメントに実際のデータや最大ページ数を持たせ、 現在表示中のページ番号はビューに持たせるのがDoc-Viewアーキテクチャに沿った方法だと思います。
それを踏まえて、サンプルを次の様に作ってみました。

testDoc.h
class CTestDoc : public CDocument
{
〜
public:
	int GetPageCount() const{
		return 4;	//ページ数
	}
	CString GetData(int page){	//0〜3
		CString str;
		str.Format("データ%d",page);	//CTestViewで表示するデータ
		return str;
	}
〜
};
testView.h
class CTestView : public CView
{
〜
// オーバーライド
	// ClassWizard は仮想関数を生成しオーバーライドします。
	//{{AFX_VIRTUAL(CTestView)
〜
	protected:
	virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
	virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
	virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
	virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo);
	//}}AFX_VIRTUAL
〜
private:
	void OnDraw(CDC *pDC,int page);
	int m_nNowPage;	//現在表示中のページ
};
testView.cpp:
CTestView::CTestView()
{
	// TODO: この場所に構築用のコードを追加してください。
	m_nNowPage=0;
}

void CTestView::OnDraw(CDC* pDC)
{
	OnDraw(pDC,m_nNowPage);
}

void CTestView::OnDraw(CDC* pDC,int page)
{
	CTestDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);

	// TODO: この場所にネイティブ データ用の描画コードを追加します。
	pDC->SetMapMode(MM_HIMETRIC);

	//赤い箱
	pDC->FillSolidRect(2000,-1000,3000,-1000,RGB(255*page/(pDoc->GetPageCount()-1),0,0));

	//黒い線
	pDC->MoveTo(2000,-1000);
	pDC->LineTo(5000,-2000);

	//青い字
	pDC->SetTextColor(RGB(0,0,255));
	pDC->SetBkColor(RGB(255,255,255));
	pDC->TextOut(2000,-2000,pDoc->GetData(page));
}

/////////////////////////////////////////////////////////////////////////////
// CTestView クラスの印刷

BOOL CTestView::OnPreparePrinting(CPrintInfo* pInfo)
{
	pInfo->SetMaxPage(GetDocument()->GetPageCount());	//最大ページ数
	if(pInfo->m_bPreview){	//印刷プレビューの時
		pInfo->m_nCurPage=m_nNowPage+1;	//現在表示中のページをプレビュー開始時のページにする
	}else{	//印刷の時
	}

	// デフォルトの印刷準備
	return DoPreparePrinting(pInfo);
}

void CTestView::OnPrint(CDC* pDC, CPrintInfo* pInfo) 
{
	// TODO: この位置に固有の処理を追加するか、または基本クラスを呼び出してください

	OnDraw(pDC,pInfo->m_nCurPage-1);
//	CView::OnPrint(pDC, pInfo);
}
今回は印刷が目的なので、画面表示の方でページを切り替える部分は省略します。

印刷は今までCView::OnPrintでやっていたのですが、ページを切り替えるために OnPrintをオーバーライドしました。そしてページを指定して描画するOnDrawを新しく設け、 画面用OnDraw印刷用OnPrintから呼び出すようにしました。
OnPreparePrintingの中で最大ページ数を指定したので、プレビューウィンドウでページを 切り替えることができます。OnPrintで渡されるpDCやpInfoはプレビューが表示したいページなので、 そのページのみを描画するようにします。
ちなみに このサンプルでは先頭ページは0ですが、 プレビューや印刷の時は1から始まるので、1を足したり引いたりしています。


指定ページの印刷

このままだと、メニューバーから「ファイル」→「印刷」を選択したり ツールバーの「印刷」ボタンをクリックしたりすると、 必ず印刷ダイアログが出てしまいます。
そこで、「全て印刷」メニューを作ってみます(つまり、印刷ダイアログを出さずに1ページから最終ページという範囲指定をします)。

testView.h:
class CTestView : public CView
{
〜
// 生成されたメッセージ マップ関数
protected:
	//{{AFX_MSG(CTestView)
	afx_msg void OnFilePrint();
	afx_msg void OnFileAllPrint();
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
〜
	int m_nNowPage;	//現在表示中のページ
	int m_nBeginPage,m_nEndPage;	//印刷範囲のページ
};
testView.cpp:
BEGIN_MESSAGE_MAP(CTestView, CView)
	//{{AFX_MSG_MAP(CTestView)
	ON_COMMAND(ID_FILE_PRINT, OnFilePrint)
	ON_COMMAND(ID_FILE_ALL_PRINT, OnFileAllPrint)
	//}}AFX_MSG_MAP
	// 標準印刷コマンド
	ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CTestView クラスの構築/消滅

CTestView::CTestView()
{
	// TODO: この場所に構築用のコードを追加してください。
	m_nNowPage=0;
	m_nBeginPage=m_nEndPage=0;
}
〜
/////////////////////////////////////////////////////////////////////////////
// CTestView クラスの印刷

void CTestView::OnFilePrint() 
{
	//印刷ダイアログを出して印刷
	m_nBeginPage=m_nEndPage=0;
	CView::OnFilePrint();
}

void CTestView::OnFileAllPrint() 
{
	//全ページ印刷
	m_nBeginPage=1;                            	//印刷開始ページ
	m_nEndPage  =GetDocument()->GetPageCount();	//最終印刷ページ
	CView::OnFilePrint();
}

BOOL CTestView::OnPreparePrinting(CPrintInfo* pInfo)
{
	pInfo->SetMaxPage(GetDocument()->GetPageCount());	//最大ページ数

	BOOL bPreview=pInfo->m_bPreview;	//プレビューフラグのバックアップ
	if(bPreview){	//印刷プレビューの時
		pInfo->m_nCurPage=m_nNowPage+1;	//現在表示中のページをプレビュー開始時のページに
		m_nBeginPage=m_nEndPage=0;
	}else{	//印刷の時
		if(m_nBeginPage>0 && m_nEndPage>0){	//印刷範囲の指定有り
			pInfo->m_bPreview=TRUE;	//プレビューのふりをすることにより、印刷ダイアログを出さない
		}
	}

	// デフォルトの印刷準備
	BOOL ret=DoPreparePrinting(pInfo);
	pInfo->m_bPreview=bPreview;	//プレビューフラグを元に戻す

	if(m_nBeginPage>0 && m_nEndPage>0){	//印刷範囲の指定有り
		PRINTDLG *p=&pInfo->m_pPD->m_pd;
		p->nFromPage=m_nBeginPage;	//印刷開始ページ
		p->nToPage  =m_nEndPage;	//最終印刷ページ
	}
	return ret;
}

ポイントは、プレビューの時は印刷ダイアログが出ないので、逆に、 印刷ダイアログを出したくない時はプレビューのふりをする という点です。
そして、本来ならDoPreparePrintingで出力される印刷ダイアログの中で 指定される印刷開始ページ・最終ページを、自分で指定します。

ページを指定する箇所で 印刷開始ページ・終了ページ共に 現在ページを指定するようにすれば、印刷ダイアログを出さずに現在ページだけ印刷することも出来ます。


VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま