デバイスコンテキストとハンドル

 ここからは少しスピードダウンです。まぁのんびり見ていきましょう。

////////////////////////////////////////////////////////////////////
// Hello World! と書き込みます。

void WriteHello( HWND p_hPaintWnd )
{
	HDC	hPaintDC;
	PAINTSTRUCT stPaint;
	char	chHello[] = "Hello, world!";

	hPaintDC = BeginPaint( p_hPaintWnd, &stPaint );

	TextOut( hPaintDC, 0, 0, chHello, strlen( chHello ) );

	EndPaint( p_hPaintWnd, &stPaint );

	return;
}
	

WM_PAINT
 さて、前回見たように、今回のWriteHello()という関数は、ウィンドウにWM_PAINTが送られてきた時に呼び出すようにしています。WM_PAINTメッセージは、ウィンドウを描画もしくは再描画しなければならないときに送られてきます。基本的には最大化したり最小化したときに送られてきます。
 渡している引数は、メッセージが送られてきた(つまり再描画するように命令された)ウィンドウのハンドルです。ウィンドウはウィンドウズが管理しているため、ウィンドウを操作するためには「ハンドル」が必要になるわけです。
 ちなみに「ハンドル」と呼ばれるものはHWNDのようにHがプレフィックスとして付いています。サイズはUINTと同じく32ビットです。一応voidポインタ、つまり何にでも使えるポインタみたいです。

ちょっと余談
 触れてませんでしたが、C++とCとでの違いについて。
 まず変数。変数は必ず関数の最初で宣言します。C++の場合にはどこでもいいんですけどね。これ忘れると無茶苦茶な数のエラーが発生するので気を付けましょう。
 もひとつ、コメントについて。C++では//ですが、Cでは本当は/*から*/までがコメントアウトされます。ただ、オプションで//も使えます。まぁ気にしなくていいでしょう。

 ああ、本当に余談だ……。

デバイスコンテキストを取得しよう
 WM_PAINTが送られてきたということで、ウィンドウを描画しなければなりません。こういう時にはまずBeginPaint()というAPIを呼び出します。この関数にウィンドウハンドルを渡すと、渡したウィンドウにくっついたデバイスコンテキストのハンドルが返ってきます。

 ではデバイスコンテキストとはなんなのでしょうか。簡単に説明すると画材と画用紙のセットのようなものです。
 まずは画用紙から。すべてのウィンドウはこのデバイスコンテキストを持ち、その画用紙がウィンドウに貼り付いているようなイメージです。
 で、その画用紙に描くための画材が、すでにデバイスコンテキストに装備されています。画材はペンやブラシ、ビットマップ、フォントやパレットといったもので、GDI(グラフィックス デバイス インターフェイス)オブジェクトと呼ばれます。

 注意しなければいけないのは、この画材は「一種類につきひとつ」ということです。例えばあるデバイスコンテキストに「MS ゴシック」というフォントが使われていたとします。この時点で文字を描き込むと当然「MS ゴシック」が使われます。
 じゃぁ「MS 明朝」を使いたいときにはというと、わざわざデバイスコンテキストに「MS 明朝」を使うようにしなければなりません。そして、この時点では「MS ゴシック」が使えなくなります。つまり、「フォント」という画材を交換したわけです。
 このように、一種類につきひとつの画材しか使えません。まぁ、ひとつの手にはいっぺんにひとつしかペンを持てないようなものでしょうかねぇ。

ハンドルずくし!
 実は、ウィンドウと同じく、デバイスコンテキストもウィンドウズに管理され、ハンドルによってのみ操作できるのです。BeginPaint()で返ってくるのはハンドルであって、デバイスコンテキストそのものではありません。例によって、デバイスコンテキストはどこか手の届かない場所に作製されています。

 さらに、実は「画材」、つまりGDIオブジェクトもウィンドウズに管理され、ハンドルによってのみ操作できるのです! 例えば、前述の「MS ゴシック」というフォント。こういう「画材」が初めから存在しているわけではありません。なぜかというと、フォントの情報にはフォント名だけではなく、文字の幅だとか高さだとか、ボールドにするかとかアンダーラインを引くかとかそういった情報も含まれるからです。こういったものを含めて「フォント」という「画材」になるわけです。
 この画材を作るには、ウィンドウを構築したときと同じようにウィンドウズに頼まなければなりません。フォントを作製するときにはCreateFont()というAPIを使います。この関数の引数に前述したフォントの情報を入れると、やはりどこかに指定したタイプのフォントが作製され、それを操作するためのHFONT、つまりフォントという画材のハンドルを取得することができるのです。

 注意しなければならないのは、こういった「画材」はウィンドウズ全体でグローバルなものだということです。フォントを作製したら、そのフォントをウィンドウズがずっと持っています。たとえアプリケーションが終了しても。つまり、作製したら削除しなければいけません。
 「リソースメーター」というアプリケーションを開くと、「GDIリソース」というインジケーターがあります。これは、作製された画材、つまりGDIオブジェクトを置いておくためのメモリの領域です。もし削除せずにおくと、そういったオブジェクトがどんどんメモリを占領していってしまうということです。全部使い切ったらまずいです。削除するにはDeleteObject()というAPIを使います。

 ただし、DeleteObject()を使う時には注意が必要です。もしそのGDIオブジェクトを他のアプリケーションが使用していた場合には、これもまたまずいです。例えば、他のウィンドウのフォントをSelectObject()を使用して他のフォントに換えたとしましょう。この関数を使うと、それまで使われていたGDIオブジェクトが返ってきます。これが「入れ換える」という意味です。
 この返ってきたオブジェクトへのハンドルを取得しなかった場合……そのGDIオブジェクトはもう誰にも手が着けられない存在ということになります。もしここで、入れ替えたオブジェクトを削除してしまったら……やっぱりこれも大変なことになってしまいますねぇ。

 こういったミスをしないためには「GDIオブジェクトをCreateする>SelectObject()で入れ換える>描画する>またSelectObject()を使って元に戻す>CreateしたGDIオブジェクトを削除する」という手順を踏むことです。めんどくさーいって感じですが、実は似たような手順をデバイスコンテキスト以外でも使うので、慣れておきましょう。

 この辺の操作はいまいち難しいので、気を付けて扱うようにしましょう。ちなみに筆者の例を言うと、ウィンドウズが所持するイメージリストというのがあって、その中にアイコンとかが入っているんですが、このイメージリストを削除しちゃいました。そしたらスタートメニューのアイコンが消えちゃった(泣)。

TextOut()
 さて、文字の描画にはTextOut()というAPIを使用します。第1引数には、文字を描き込むデバイスコンテキストのハンドルを渡します。第2、第3引数は描画する位置、第4引数には書き込む文字列、第5引数にはその文字列の長さを渡します。
 つまり、フォントや文字の色などは全然指定されていません。こういった「画材」は前述したように事前に入れ換えておかなければならないということです。
 ちなみに、文字列についての説明は次回行う予定です。

EndPaint()
 描画をしたら、EndPaint()を呼び出して「描画が終了しましたよー」と知らせる必要があります。それだけです。
 ちなみにPAINTSTRUCT構造体へのポインタを渡していますが、この構造体は基本的に使わないので気にしないでください。

MFCと「オブジェクト指向」
 MFCでは、WM_PAINTメッセージのハンドラ関数はCWnd::OnPaint()と決まっています。このハンドラを作製すると、必ず関数の最初に「CPaintDC dc(this); // 描画用のデバイス コンテキスト」という行が書かれています。
 BeginPaint()なんかはどこに行ったのかというと、実はCPaintDCというクラスの中に入っています。

 クラスには「コンストラクタ」と「デストラクタ」という便利な機能があります。「コンストラクタ」はそのクラスの型の変数が宣言されたときに呼び出される関数で、逆に「デストラクタ」は変数が破棄されたときに呼び出される関数です。
 この「コンストラクタ」の中でBeginPaint()が、「デストラクタ」の中でEndPaint()が呼び出されています。つまり、上のようにCPaintDC dc(this);と宣言されただけでBeginPaint()が呼び出され、CWnd::OnPaint()が終了したときにEndPaint()が呼び出されるというわけです。

 で、実際に文字の描画はどのようにするかというと、CDCクラスを使用します。CPaintDCCDCから派生しているので、CDCの機能をそのまま使うことができます。で、CDCの中に、デバイスコンテキストへのハンドルが入っています。
 CDCクラスにはデバイスコンテキストに描画するための様々なメンバ関数が装備されています。これらのメンバ関数は、通常は同名のAPIを中で呼び出しているだけです。例えばSelectObject()TextOut()といったAPIと同名のメンバ関数がCDCにはあり、これらのメンバ関数が結局はSelectObject()といったAPIを呼び出しているわけです。

 さらに「画材」つまりGDIオブジェクトも、同じ機能を持つクラスが存在します。ペンならCPen、フォントならCFontといった具合です。これらのクラスはGDIオブジェクトのハンドルを持ち、そのハンドルを使用するAPIと同名のメンバ関数を持っているわけです。
 では、なぜこのようなことをしているのかというと、それには「オブジェクト指向プログラミング」というものが関わってきます。

 C++言語は「オブジェクト指向な言語」と一応言われます。「オブジェクト指向」とは言ってみれば「物」中心のシステムです。
 例えば、ハサミ。ハサミはちょっきんちょっきんと紙を切るものです。カッターのように紙を切ったり、刃で釘を挟んで引き抜いたり、ましてや人を刺したりするためには作られてはいません。もちろん、そういう風に使うことはできますが、ハサミはそれを望んでいません。もちろん、危険でもあります
 こういった部分を「使用者」中心のシステムとすれば、「物」中心のシステムとはそういった「ハサミの望まない使われ方」をさせないシステムです。オブジェクト指向の世界では、「ハサミ」というオブジェクトはカッターのように紙を切ったりはできないのです。

 これをC++の「クラス」に当てはめてみましょう。ウィンドウへのハンドルも、デバイスコンテキストへのハンドルも、GDIオブジェクトへのハンドルも、すべて同じハンドルです。簡単な型キャストで他のハンドルへと変わってしまいます。もし、ウィンドウへのハンドルをTextOut()に渡してしまったら……。
 これを防ぐために、ウィンドウハンドルはCWnd、デバイスコンテキストはCDCと、それ専用のクラスが作製されていて、そのハンドルでしか使用できないAPIが、メンバ変数として装備されているわけです。プログラマは、クラスにハンドルを結びつけるときだけ注意すればいいのです。CWndにはTextOut()というメンバ関数は存在しないので、前述のような問題は起き得ません。
 このように、オブジェクト指向を利用すると「オブジェクト」を安全に操作することができるようになるのです。

 とはいえ、オブジェクト指向にも問題点はあります。例えば、クラスにハンドルを割り当てるにはAttach()というメンバ関数を使用します。ハンドルを割り当てたあと、このクラスが破棄されたときにデストラクタが呼び出されますが、実は自動的にDeleteObject()が呼び出され、オブジェクトが破棄されてしまうのです。これを防ぐためには、破棄される前にDetach()というメンバ関数を呼び出さなければなりません。
 この問題は、プログラムがすべてオブジェクト指向化されていないことに原因があります。

 ハンドルを扱うクラスは、あくまでハンドルを持っているだけです。ですが、上のようなクラスの機能を考えると、このようなクラスはオブジェクトそのもの(つまりウィンドウであったりデバイスコンテキストであったり)として見なされているように見えます。
 もしこれが、クラスだけを使用する場合には問題ありません。例えばCDCにはGetWindow()というメンバ関数があります。この関数は、デバイスコンテキストが貼られたウィンドウを取得する関数です。この関数で取得できるのは、CWndへのポインタです。ポインタは破棄されてもデストラクタは呼び出されません。これなら、勝手にオブジェクトが削除されるという問題は回避できます。
 ところが、ウィンドウズプログラミングではすべてをクラスだけで使用するわけにはいきません。時にはAPIを使用する必要があります。クラスのメンバ関数として装備されていなかったり、望んだような結果をもたらさなかったり……こういった場合にはハンドルを使用せざるを得ません。そして、バグの可能性が生まれるというわけです。

 筆者としては、クラスをできる限り使用することを奨めます。AppWizardを使用してアプリケーションを作製する限り、MFCの呪縛から逃れることはできません。ハンドルとクラスの混在は非常にバグを生み出しやすい環境だと言えるので、統一するのであればクラスの方、というわけです。
 それに、クラスのメンバ関数は、APIでの呼び出しが難しいものを簡単な形に変えてくれていたりします。デバッグ用の環境も整えられています。もちろんリファレンスはすべて日本語です。さらに、将来ウィンドウズ98が現れ、APIの仕様が変更されても、MFCがその部分を吸収してくれる可能性があります。こういった利点を考えると、MFCを中心に使用した方がいいのではないでしょうか。
 もっとも、こういう部分にバグが潜んでたりするんだけどね……。

今回は文字ばっかりやねぇ……
 なんかごたく並べてるだけでなんの役にも立ってないような今回ですが、いかがだったでしょう。次回はひーっCString使ってくれないと文字列操作できないよぉ!!という方々のためにその辺の部分を見ていきたいと思います。
 で、次回で前半が終了。後半はメニューやダイアログ、アイコンといった「リソース」について見ていこうかなと思っています。

 やっぱり、なんか盛り下がっていくなぁ……。

(C)KAB-studio 1997, 1998 ALL RIGHTS RESERVED.