デバイスコンテキスト(DCと略されることが多い)は、よく出てくる割に分かりにくい代物だと思います。 僕もいまだによく分かっていませんが、使っているうちに「こういうことではないか」と思うようになったので、 それについて書いておきたいと思います。 以下、断定口調で書いていますが、あくまで単なる想像なので、間違っている可能性がいつにも増して大ですので注意して下さい。
デバイスコンテキストとは、プリンタや画面等に描画するためのインターフェースです。
プログラマからすれば、画面に描画してあるものをプリンタに描画するなら 画面もプリンタも同じようなものですが、
実際のハードウェアは異なるものです。その差異を埋め、同じようなものとして扱うのがデバイスコンテキストです。
デバイスコンテキストは あくまでアプリケーションと画面を結ぶインターフェース つまり受け渡し役に過ぎず、
描画されたデータを保持しているわけではありません。
その関係は、次の様になります。
アプリケーション | デバイスコンテキスト | 画面・プリンタ・ビットマップ等 | |
---|---|---|---|
保持データ | ペンやブラシを保持 | アプリから指示されたペンやブラシを記憶だけしておく | 実際の画像の保持 |
動作 | 描画命令などを出す | 上記のペンやブラシを使って画像に描画する | 実際の画像データを書き換える |
以下、もう少し具体的に見ていきましょう。
普通にAppWizardを使ってSDIやMDIのアプリケーションを作ると、以下の様なコーディングがされています。
dcView.cpp:///////////////////////////////////////////////////////////////////////////// // CDcView クラスの描画 void CDcView::OnDraw(CDC* pDC) { CDcDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: この場所にネイティブ データ用の描画コードを追加します。 }
CViewの派生クラスのOnDrawに「CDC *pDC」が渡されています。
CDCがデバイスコンテキストを扱うクラスです。これを使って描画するのはご存知の通りです。
具体的なやり方はどこの入門書を見ても載っていますが、例えば下の様になります。
void CDcView::OnDraw(CDC* pDC) { CDcDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //ペンを作成 CPen pen(PS_SOLID,1,RGB(0,0,255)); //作成したペンをDCに選択(同時に、古いペンを保管) CPen *pOldPen=pDC->SelectObject(&pen); //ペンを使って線を描く pDC->MoveTo(0,0); pDC->LineTo(64,64); //DCのペンを元に戻す pDC->SelectObject(pOldPen); //作成したペンを削除 pen.DeleteObject(); }
このときの動作を、OnDrawが呼ばれる前の処理から見ていきます。
場所 | アプリケーション | デバイスコンテキスト | 画面等 | |
---|---|---|---|---|
1 | システム | デフォルトのペンやブラシを作成(ペンSとする) | ||
2 | システム | デバイスコンテキストを作成 | 作成された | |
3 | システム | 1で作ったペンやブラシを、DCが使うべきペンやブラシとして指示 | 指示されたペンやブラシを覚えておく(ペンSを記憶) | |
4 | アプリ | ペンを作成(ペンAとする) | ||
5 | アプリ | ペンAを選択(SelectObject) | ||
今まで記憶していたペン(ペンS)をアプリに渡し、新しいペン(ペンA)を記憶する | ||||
渡された古いペン(ペンS)を覚えておく(pOldPen) | ||||
6 | アプリ | MoveToを実行 | 描画用座標を、指定された場所に記憶し直す | |
LineToを実行 | 記憶している座標とペン(ペンA)を使って線を描く | DCから指示された通りに画像を変更 | ||
7 | アプリ | 古いペン(ペンS)を選択(SelectObject) | ||
今まで記憶していたペン(ペンA)をアプリに渡し、新しいペン(ペンS)を記憶する | ||||
返されたペン(SelectObjectの返り値)はペンAだと分かっているので、無視している | 結果として、ペンは以前の状態に戻った | |||
8 | アプリ | ペンAを削除(DeleteObject) | ||
9 | システム | ペンやブラシをDCに解放させる | ペン(ペンS)やブラシを記憶から消す | |
10 | システム | 2で作成したデバイスコンテキストを解放 | 削除された | |
11 | システム | 1で作成したペン(ペンS)やブラシを削除 |
ペンを作って保持しているのは、あくまでアプリケーション(システム)です。
デバイスコンテキストは、アプリケーションが保持しているペンを「これを使え」と指示されて
そのペンの場所を記憶しているだけです。
しかも記憶力は貧弱で、ペンやブラシ等の種類毎に1つずつしか記憶できません。
そこでSelectObjectでは、現在記憶しているペンと新しく指示されたペンを交換する形をとります。
古いペンはアプリケーション側に覚えておいてもらい、後でそれを教えてもらうのです。
デバイスコンテキストが記憶しているペンやブラシは アプリケーションでは削除できないので、
古いペンを元に戻すことで、アプリケーションが指示したペンを解放させることになります。
画面が実際に変更になっているのは、LineToが実行された時だけです。
ペン等がいくら交換されても、実際の画面には何ら関係無いのです。
MoveToも、デバイスコンテキストが保持している描画用の座標を変更しているだけです。
よく使われるデバイスコンテキストには、画面用・プリンタ用の他に
メモリデバイスコンテキスト(メモリDC)というものがあります。
これは通常「裏画面」と呼ばれる、画面と同等の構造を持つデータをメモリ上に作るためのものです。
これは、以下の様にコーディングされます。
void CDcView::OnDraw(CDC* pDC) { CDcDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //メモリDCを作成 CDC memDC; memDC.CreateCompatibleDC(pDC); //メモリDCの実体となるビットマップを作成 CBitmap memBmp; memBmp.CreateCompatibleBitmap(pDC,128,64); //メモリDCにビットマップを選択 CBitmap *pOldBmp=memDC.SelectObject(&memBmp); //メモリDC(即ちビットマップ)に描画 memDC.FillSolidRect(0,0,128,64,RGB(0,0,255)); //メモリDCの画像(即ちビットマップの内容)を実際の画面に転送 pDC->BitBlt(32,32,128,64,&memDC,0,0,SRCCOPY); //メモリDCのビットマップを戻す memDC.SelectObject(pOldBmp); //ビットマップを削除 memBmp.DeleteObject(); //メモリDCを削除 memDC.DeleteDC(); }
CDC::CreateCompatibleDCを使うことで、メモリDCを作成できます。
使える色数等は、引数に指定したpDCと同じになります。
重要な点は、画面用DCが「画面に対して描画等を行う」のに対し、
メモリDCは「自分の中に指定されたビットマップに対して描画等を行う」ということです。
したがって、メモリDCには別途ビットマップを指定してやる必要があります。
実はCreateCompatibleDCを行った時点でデフォルトのビットマップが
メモリDC内部で指定されているのですが、このビットマップはモノクロで、サイズが1×1ドットしかありません。
これでは使い物にならないので、自分でビットマップを指定するのです。
デフォルトで1×1サイズのビットマップを持つくらいなら、サイズも指定してビットマップを作ってくれれば
良さそうなものなのに…。
なお、これで分かる通り、デバイスコンテキスト自体は画像サイズを持っていません。
ビットマップはCreateCompatibleBitmapで作るのが一番簡単です。
引数のpDCで指定された画面と同等の色数等を持てるようになりますが、
サイズは画面のものが引き継がれないので 別途指定することになっています。
メモリDCにビットマップを指定するには、ペンと同じでSelectObjectを使います。
ペンとの大きな違いは、デバイスコンテキスト内部での使われ方です。
ペンが 線の「描かれ方」として使われるのに対し、メモリDCのビットマップは
「描かれる対象」そのものです。
また、画面用DCは「描かれる対象」が画面なので、意味のあるビットマップは持っていません。
画面用DCである「OnDrawの引数pDC」に対してビットマップを選択してやっても、
画面がそのビットマップの画像に替わるわけではないのです。
pDCにビットマップを選択することはできますが、そうしてから描画命令を出しても
ビットマップには何も影響なく、きちんと画面に描画されます。
逆に、画面に描かれている画像を取得するつもりで、SelectObjectの返り値であるビットマップを使おうとしても無理です。
NULLが返って来るだけです。
したがって、画面とビットマップ間で画像をやりとりするには、
画面用DCとメモリDCを介してCDC::BitBltを使うしかないのです。
ちなみに本筋とは関係ありませんが、DeleteObjectやDeleteDCは それぞれCBitmapやCDCのデストラクタで呼ばれるので、特にコーディングする必要はありません。