Windows API - グラフ描画クラス編 第1回 〜デバイスコンテキスト〜

ウィンドウとデバイスコンテキスト

PRINTSTRUCT 構造体と WM_PAINT メッセージ

ウィンドウに無効領域と呼ばれる領域が作られると、WM_PAINT メッセージが送出されます。 WM_PAINT メッセージを処理する場合は、まず、PAINTSTRUCT 構造体と呼ばれるものを用いて、 無効領域の情報を取得する必要があり、それが、

HDC BeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint);

という API 関数です。第2引数には、PAINTSTRUCT 構造体のポインタを渡す必要があります。

ここで発行されるのが、デバイスコンテキストハンドル HDC なわけで、これを使って 無効領域を再描画します。再描画を行った後は、

BOOL EndPaint(HWND hWnd, const PAINTSTRUCT* lpPaint);

とする必要があります。

TextOut() API 関数

BeginPaint() API 関数で発行される HDC を用いると、

BOOL TextOut(HDC, int, int, LPCWSTR, int);

のように、第1引数に HDC をとる API 関数を用いて描画することが出来ます。

TextOut() API 関数は、第4引数に Unicode 文字列リテラルを指定することから 想像できるように、文字列を描画する関数です。

第2引数と第3引数が、描画される (x,y) クライアント座標を表し、 第5引数が Unicode 文字列リテラルの長さを表します。

この関数を、BeginPaint() と EndPaint() の間で呼ぶことにより、 ウィンドウがアクティブでなくなったり、何かで隠されてしまったとしても、 アクティブになったときに、再描画されます。

COLORREF 構造体と RGB マクロ

恐らく、TextOut() API 関数を利用して表示されたものは、文字の色が黒、 背景色が白となっています。この色を変える場合、TextOut() を呼ぶ前に、

BOOL SetBkColor(HDC, COLORREF);
BOOL SetTextColor(HDC, COLORREF);

という2つの API 関数を呼びます。前者が背景色、後者が文字色となります。

ここで、COLORREF 構造体が色を表現するものとなりますが、これは、

RGB(r,g,b)

というマクロを使うことで指定できます。ここで、r, g, b はそれぞれ BYTE 型の値で、 0x00 から 0xFF までで赤、緑、青の成分を指定します。もちろん、全て 0x00 ならば黒、 全て 0xFF ならば白となります。

RECT 構造体と DrawText() API 関数

RECT 構造体は、ある長方形の左上の座標と、右下の座標を表します。この構造体は、 第1引数に RECT 構造体へのポインタをとる、

BOOL SetRect(
 LPRECT lpRect, int xLeft,int yTop, int xRight, int yBottom
);

という API 関数を用いて初期化することが出来ます。 このようにして指定した長方形の範囲内に文字列を整形して描画するのが、

BOOL DrawText(HDC, LPCWSTR, int, LPRECT, UINT);

という API 関数です。第2引数は Unicode 文字列リテラルですが、これが、 TEXT() マクロを使って生成したものである限りにおいて、第3引数は -1 とすることで、 自動的に文字列の長さを計算します。また、第4引数が RECT 構造体へのポインタです。

第5引数ですが、次のようなフラグを指定することが出来ます。

DT_CENTER        中央揃え
DT_LEFT          左揃え
DT_RIGHT         右揃え
DT_WORDBREAK     自動複数行表示
DT_WORD_ELLIPSIS 自動省略表示

HDC を引数にとる API 関数

ここでは、TextOut() や DrawText() のような 第1引数に HDC をとる描画関数のうち、 代表的なものを挙げていきます。

MoveToEx() API 関数と LineTo() API 関数

直線を描画するための API 関数です。

BOOL MoveToEx(HDC, int, int, LPPOINT);
BOOL LineTo(HDC, int, int);

MoveToEx() API 関数も LineTo() API 関数も、これから描画する点の位置を、 第2引数と第3引数で指定されるクライアント座標に変更しようとしますが、 前者は線を引かず、後者は直前に指定されていた座標から直線を引きます。

MoveToEx() API 関数の第3引数には、POINT 構造体というものの ポインタを渡すことが出来、MoveToEx() で移動する前の位置を取得できますが、 別に必要なければ、NULL を指定します。

つまり、連続して直線を引く場合には、

MoveToEx(hDc,x1,y1,NULL);
LineTo(hDc,x2,y2);
LineTo(hDc,x3,y3);
...

などとします。こうすることで、(x1,y1) -> (x2,y2) -> (x3,y3) と 黒い直線が引かれます。

Rectangle() API 関数

その名の通り、長方形を描画する API 関数です。

BOOL Rectangle(HDC, int, int, int, int);

第1引数が HDC である以外は、残りの4つの引数は、SetRect() と同じです。

この4つの引数で指定した長方形の座標に対して、白い背景色で黒い境界線を持つ 長方形が描画されます。

Ellipse() API 関数

楕円を描画する API 関数です。

BOOL Ellipse(HDC, int, int, int, int);

Rectangle() と全く同じ引数ですが、これは、Rectangle() で描画される長方形に 内接するような、白い背景色で黒い境界線の楕円を描画します。

クライアント DC とメモリ DC

スタティックコントロール

さて、で、デバイスコンテキストというのは、そのウィンドウに付属している、 まぁ、画用紙というか画材というか、そういうものの組み合わせというか、 パッケージというかセットのようなものです。

これまで、親ウィンドウのウィンドウプロシージャ内で WM_PAINT メッセージを処理していたことから 分かるように、親ウィンドウの DC を取得して直接描画していましたが、これから先は、 スタティックコントロールという子ウィンドウの DC に描画することを考えてみます。

HWND hStatic=CreateWindow(
 TEXT("STATIC"), TEXT(""), 
 WS_CHILD | WS_VISIBLE | SS_BITMAP,
 x, y, width, height, 
 hParentWnd, NULL, hInstance, NULL
);

早速 DC を取得したいところですが、先程とは異なり、ウィンドウプロシージャは 今のところどこにも見えないため、無効領域が発生したときに再描画させようと BeginPaint() を 使おうとしてもどうにも使えません。

そこで、ひとまず描画するための DC である、メモリ DC というものを発行してみます。

HDC CreateCompatibleDC(HDC);
BOOL DeleteDC(HDC);

CreateCompatibleDC() API 関数によって指定した DC に対するメモリ DC へのハンドルが発行されます。 また、このようにして発行したメモリ DC は、必ず、DeleteDC() API 関数で解放する必要があります。

で、肝心のスタティックコントロールの DC である、クライアント DC ですが、これは、

HDC GetDC(HWND);
BOOL ReleaseDC(HWND, HDC);

という、ウィンドウハンドル hStatic を引数に取る API 関数を使うことで発行します。 ここで、発行したクライアント DC ハンドルは必ず ReleaseDC() API 関数で解放する必要があります。

つまり、順序としては、まず、GetDC() でクライアント DC を取得し、 このクライアント DC をもとに、CreateCompatibleDC() でメモリ DC を取得します。

必要な作業が終わり次第、まず、クライアント DC を ReleaseDC() で解放し、 アプリケーションが終了するまでのどこか、遅くとも WM_DESTROY メッセージを受けた 時には、DeleteDC() でメモリ DC を解放します。

InvalidateRect() API 関数

さて、本来のスタティックコントロールの DC であるクライアント DC ではなく、 メモリ DC へのハンドルを使って描画しようとしているので、 実は、どんなに描画しようと、決してクライアント DC に描画されることはありません。

これまで、WM_PAINT メッセージの位置で処理していたことから分かるように、 Windows は、無効領域が現れてはじめてそれを描画します。

そこで、意図的に無効領域を作って、そのときに、メモリ DC の内容をそのまま、 クライアント DC にコピーしようと考えてみます。

BOOL InvalidateRect(HWND, const RECT*, BOOL);

そこで使うのが、この InvalidateRect() という 第1引数で指定したウィンドウの第2引数で指定した長方形の範囲を無効領域にする API 関数です。 ここで、第3引数は再描画のときに背景色を消去するかどうかを指定します。

ここでは、クライアント DC の全面を無効領域とするために、第1引数は hStatic、 第2引数は NULL、第3引数は TRUE としてみます。

これにより、スタティックコントロールに、WM_PAINT メッセージが発行されます。

サブクラス化

元はと言えば、スタティックコントロールの WM_PAINT が処理できないから、 いろいろごちゃごちゃとやってるっていうのに、WM_PAINT を発行してどうすんだよっていう話ですが、 それを解決するのがサブクラス化です。

SetWindowLong() API 関数

GetWindowLong() はインスタンスハンドルを取得するために前に出てきましたが、

LONG SetWindowLong(HWND, DWORD, LONG);

という API 関数を使うと、逆に第3引数の値に変更することが出来ます。 変更前の値は、LONG 型で返ってきます。

ここで、第2引数には、次のようなものが指定できます。

GWL_WNDPROC    (WNDPROC)   ウィンドウプロシージャ
GWL_HINSTANCE  (HINSTANCE) インスタンスハンドル
GWL_HWNDPARENT (HWND)      親ウィンドウのハンドル
GWL_STYLE      (DWORD)     ウィンドウスタイル

つまり、GWL_WNDPROC なるものを第2引数に指定すると、 実は、ウィンドウプロシージャを取得すると同時に、全く違うウィンドウプロシージャに 変更することが出来ます。

CallWindowProc() API 関数

つまり、全く新しいコールバック関数を作り、その中で WM_PAINT を処理します。 そして、残りの作業は本来のウィンドウプロシージャに任せようというわけですね。

というわけで、残りの作業というものを代行してくれるのが、

LRESULT CallWindowProc(WNDPROC, HWND, UINT, WPARAM, LPARAM);

という API 関数です。似たようなことをしてくれる、DefWindowProc() という API 関数が ありましたが、その4つの引数の前に WNDPROC 型の引数が追加されます。

つまり、ここに、SetWindowLong() API 関数の戻り値を WNDPROC 型にキャストしたものを ぶちこむことで、本来のウィンドウプロシージャを呼び出すことが出来るわけですね。

以上を踏まえた上で、新しいウィンドウプロシージャを書くと、

WNDPROC prevWndProc;
LRESULT CALLBACK StaticProc(
 HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
 HDC hDc;
 PAINTSTRUCT ps;
 switch(uMsg){
 case WM_PAINT:
  hDc=BeginPaint(hWnd,&ps);
  ...
  EndPaint(hWnd,&ps);
  break;
 default:
  return CallWindowProc(prevWndProc, hWnd, uMsg, wParam, lParam);
 }
 return 0;
}

などという風に WM_PAINT が処理できるわけですね。

後は、どこか別の場所で、 SetWindowLong() API 関数を使って、スタティックコントロールのウィンドウプロシージャを、 この StaticProc に変更し、その時の戻り値を prevWndProc に キャストすることで、WM_PAINT を処理することが出来ます。

このような手法を、サブクラス化というわけですね。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])