2010年5月12日星期三

Windowsグラフィックス諸概念


Last modified on
06.04.03

Windowsグラフィックス諸概念

まず、Windows のグラフィックスをつかう上で絶対に知っておかなければならないことがあります。それは、「デバイスコンテキスト」と「GDI オブジェクト」と「描画メソッド」です。デバイスコンテキスト、GDI オブジェクトと描画メソッドをまとめて、GDI (Graphic Devece Interface) といいます。

ここで話を簡単にするために、スケッチブックに絵を描くことを考えてみましょう。この時点で、スケッチブックの一枚一枚の紙に相当するのが、デバイスコンテキストです。スケッチブックに絵を書いていくように、プログラマはデバイスコンテキストに記述していくことになります。さらに、絵を書くためには、ペンや鉛筆などが必要です。これに相当するものが、 GDI オブジェクトです。GDI オブジェクトで、ペンの色をや、ブラシの色を指定してやることで、デバイスコンテキストに描画ができるようになります。そして実際に描画を行うのが描画メソッドです。この描画メソッドをつかうことによって、ラインや円を書いていくことができるようになります。

以上の3つについてわかっていれば、ここでの説明はもうほとんどありません。あとは実際にどのように描画を行うのか見ていってみましょう。

目次へ


実際に描画

それでは実際に描画を行ってみましょう。普通にウィンドウを作成して、その上にラインと円を描画してみましょう。その前に下準備をしておきます。もう少しプログラムが楽になるように、同じようなプログラムは別ファイルにまとめてしまいましょう。(WinMain関数も含む)

userlib.h

/*******************************************************************************

* ユーザー定義ライブラリヘッダー <>

*

* userlib.c のヘッダーファイル

*

*******************************************************************************/

#ifndef _USERLIB_H_

#define _USERLIB_H_

/********************************************************************************

* 定数の定義 ここに定数を定義していきます

********************************************************************************/

#define APP_NAME "TESTAPP"

#define APP_TITLE "テストアプリケーション"

/********************************************************************************

* 構造体 ここに構造体を定義していきます

********************************************************************************/

/********************************************************************************

* 関数のプロトタイプ

********************************************************************************/

ATOM MyRegisterClass(HINSTANCE hInstance, LPCTSTR lpszMenuName);

BOOL InitInstance(HINSTANCE hInstance, HMENU hMenu);

WPARAM EasyMessageLoop();

/* これは必ず作成すること */

LRESULT CALLBACK WndMainProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

#endif

/*** end of file ****************************************************************/

userlib.c

/********************************************************************************

* ユーザー定義ライブラリ <>

*

* 自分にとって便利な関数を定義しておきます

********************************************************************************/

#define WIN32_LEAN_AND_MEAN

#include

#include "userlib.h"

/********************************************************************************

* ウィンドウクラスの定義

*

* hInstance : アプリケーションハンドル

* lpszMenuName : メニュー名

*

* 戻り値 : 成功したら固有のアトムを返す; 失敗したら 0

********************************************************************************/

ATOM MyRegisterClass(HINSTANCE hInstance, LPCTSTR lpszMenuName)

{

WNDCLASSEX wcex;

wcex.cbSize = sizeof(wcex);

wcex.style = CS_HREDRAW | CS_VREDRAW;

wcex.lpfnWndProc = WndMainProc;

wcex.cbClsExtra = 0;

wcex.cbWndExtra = 0;

wcex.hInstance = hInstance;

wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);

wcex.hCursor = LoadCursor(NULL, IDC_ARROW);

wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

wcex.lpszMenuName = lpszMenuName;

wcex.lpszClassName = APP_NAME;

wcex.hIconSm = NULL;

return RegisterClassEx(&wcex);

}

/********************************************************************************

* ウィンドウの作成

*

* hInstance : アプリケーションハンドル

* hMenu : メニューハンドル

*

* 戻り値 : 成功したら TRUE; 失敗したら FALSE

*

********************************************************************************/

BOOL InitInstance(HINSTANCE hInstance, HMENU hMenu)

{

HWND hWnd = NULL;

if((hWnd = CreateWindowEx(

WS_EX_APPWINDOW,

APP_NAME,

APP_TITLE,

WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT,

CW_USEDEFAULT,

CW_USEDEFAULT,

CW_USEDEFAULT,

NULL,

hMenu,

hInstance,

NULL)) == NULL) return FALSE;

ShowWindow(hWnd, SW_SHOWNORMAL);

UpdateWindow(hWnd);

return TRUE;

}

/********************************************************************************

* 簡易メッセージループ

*

* 戻り値 : WM_QUIT メッセージからの戻り値

********************************************************************************/

WPARAM EasyMessageLoop()

{

MSG msg;

while(GetMessage(&msg, NULL, 0L, 0L))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

return msg.wParam;

}

/********************************************************************************

* メイン関数

********************************************************************************/

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nShowCmd)

{

if(!MyRegisterClass(hInstance, NULL)) return 0;

if(!InitInstance(hInstance, NULL)) return 0;

return EasyMessageLoop();

}

/*** end of file ****************************************************************/

上記2つのファイルを一緒にコンパイルしてください。上の関数は、前回の例題でつかったものとほぼいっしょです。これからは、ここに定義してある関数は、あたりまえのものとしてつかっていきます。自分でも便利な関数を作ったりしたら、ここに追加していって自分独自の便利な関数定義ファイルを作っていきましょう。(注:これらの関数は、プロジェクトに追加しないと使用できませんから、必ずプロジェクトに追加しておいてください)

それでは本題に戻ります。ウィンドウを作成して、その上にラインと円を表示してみます。ウィンドウは、描画されるときに WM_PAINT メッセージを呼び出します。このメッセージは、ウィンドウが再描画されるとき、例えば最小化状態から復帰したり、一部分がほかのウィンドウに隠されているときに、アクティブになる時などに呼び出されます。このメッセージを拾って、ウィンドウに描画してみましょう。

/********************************************************************************

* グラフィックを描いてみよう1

********************************************************************************/

#define WIN32_LEAN_AND_MEAN

#define STRICT

#include

#include "userlib.h"

void OnPaint(HDC hDC);

/********************************************************************************

* コールバック関数

********************************************************************************/

LRESULT CALLBACK WndMainProc(HWND hWnd, UINT Msg,

WPARAM wParam, LPARAM lParam)

{

PAINTSTRUCT ps;

HDC hDC = NULL;

switch(Msg)

{

case WM_DESTROY:

PostQuitMessage(0);

break;

case WM_PAINT:

hDC = BeginPaint(hWnd, &ps);

OnPaint(hDC);

EndPaint(hWnd, &ps);

break;

default:

return DefWindowProc(hWnd, Msg, wParam, lParam);

}

return 0L;

}

/********************************************************************************

* メッセージ処理

********************************************************************************/

void OnPaint(HDC hDC)

{

MoveToEx(hDC, 10, 10, NULL);

LineTo(hDC, 30, 10);

Ellipse(hDC, 0, 0, 30, 20);

}

さて、見て分かるようにデバイスコンテキストの取得には BeginPaint 関数をつかっています。デバイスコンテキストは、ハンドルの形で取得できます。あとはこのハンドルをつかって、線を描いたり、楕円を描いたりするだけです。簡単ですよね。デバイスコンテキストの使用の終了には、EndPaint 関数をつかいます。これで描画の終了です。この2つの関数は、相関関係にあるので、必ずいっしょに用いてください。また、この2つの関数は WM_PAINT メッセージ内でしか使えません。それ以外の場合は、GetDC / ReleaseDC をつかいます。WM_PAINT 以外でグラフィックの処理を行いたい場合は、この2つの関数を用いましょう。PAINTSTRUCT 構造体については、普通のユーザーはつかうことはないので深く考える必要はないですよ。BeginPaint について MSDN ライブラリ内には次のようにかかれています。

ref . An application should not call BeginPaint except in response to a WM_PAINT message. Each call to BeginPaint must have a corresponding call to the EndPaint function. (About BeginPaint) (任意のアプリケーションは、WM_PAINT以外のメッセージ処理においてBeginPaintを呼び出してはいけない。云々…)

実際の描画には、 MoveToEx / LineTo / Ellipse 描画メソッドをつかっています。 MoveToEx 関数は現在の描画ポイント(ラインの引き始めの点)を移します。 LintTo 関数は、現在の描画ポイントから指定の描画ポイントまでラインを引きます。 Ellipse 関数は、楕円(円)を描きます。関数の説明については、MSDN ライブラリを見てくださいね。

こんな感じで、結構簡単にグラフィックを描くことができます。ここではデバイスコンテキストと描画メソッドをつかってみました。つぎは GDI オブジェクトをつかってみましょう。

目次へ


GDIオブジェクト

それでは GDI オブジェクトをつかってみましょう。そのまえに、 GDI オブジェクトについてもう少し詳しく説明しておきましょう。デバイスコンテキストに描画するには、ペンやブラシの属性が決まっていなければなりません。ペンというのは、名前のとおり直線や円を描くときに線を引く属性のことです。使用するペンによって太さが変わったり、直線が破線になったりします。ブラシは塗りつぶしの GDI オブジェクトで、塗りつぶすときの属性が指定されています。ウィンドウズでは、あらかじめデフォルトのペンやブラシが設定されています。自分で変更したいときは、自分で作成して変更することになります。

GDI オブジェクトには、次の7種類あります。

· ビットマップ

· ブラシ

· フォント

· 論理パレット

· パス

· ペン

· リージョン

このなかで、ビットマップ、ブラシ、フォント、ペンを紹介していきます。 GDI オブジェクトは OS の内部で保存されるので、ユーザー自体はハンドルの形で扱うことになります。つまり、GDI オブジェクトを取得するときに、OS からハンドルをもらい、つかう GDI オブジェクトによってハンドルを OS に指定することになります。ほとんど OS 任せなので、簡単でしょう。ユーザーのすることは GDI オブジェクトのハンドル(カギ)をなくさないように管理することです。このカギを無くしてしまうと、作成した GDI オブジェクトにアクセスできないだけではなく、開放もされないので、メモリにずっと残ることになります。いわゆる「オブジェクトリーク」を起こしてしまうので、絶対なくさないようにしましょう。

目次へ


ペンオブジェクトを使ってみる

さて、それでは実際に ペンオブジェクトを作成してみましょう。次のプログラムを見てください。今回は WM_PAINT をつかわずに、WM_LBUTTONDOWN をつかってみましょう。このメッセージを引っ掛けて描画してみます。

/********************************************************************************

* GDIオブジェクトをつかってみよう

********************************************************************************/

#define WIN32_LEAN_AND_MEAN

#define STRICT

#include

#include "userlib.h"

void OnLButtonDown(HWND hWnd, POINT p);

/********************************************************************************

* メッセージプロシージャ

********************************************************************************/

LRESULT CALLBACK WndMainProc(HWND hWnd, UINT Msg,

WPARAM wParam, LPARAM lParam)

{

POINT p;

HDC hDC = NULL;

switch(Msg)

{

case WM_DESTROY:

PostQuitMessage(0);

break;

case WM_LBUTTONDOWN:

p.x = LOWORD(lParam);

p.y = HIWORD(lParam);

OnLButtonDown(hWnd, p);

break;

default:

return DefWindowProc(hWnd, Msg, wParam, lParam);

}

return 0L;

}

/********************************************************************************

* メッセージ処理

********************************************************************************/

void OnLButtonDown(HWND hWnd, POINT p)

{

HDC hDC = NULL;

HPEN hOldPen = NULL, hPen = NULL;

// デバイスコンテキストの取得

hDC = GetDC(hWnd);

// ペンの作成

hPen = CreatePen(PS_DOT, 3, RGB(0, 255, 128));

// ペンの指定

hOldPen = (HPEN)SelectObject(hDC, hPen);

// 円を描く(半径10)

Ellipse(hDC, p.x - 10, p.y - 10, p.x + 10, p.y + 10);

// ペンを元のペンに戻す

SelectObject(hDC, hOldPen);

// 作成したペンオブジェクトを抹消

DeleteObject(hPen);

// デバイスコンテキストの開放

ReleaseDC(hWnd, hDC);

}

このようにペンオブジェクトをつかいます。ここでは、デバイスコンテキストの作成に GetDC ReleaseDC を用いています。

ペンの作成には、 CreatePen 関数をつかいます。作成したペンをデバイスコンテキストに指定するには、 SelectObject 関数をつかいます。ペンをデバイスコンテキストに指定しないと、作成したペンを使えませんので注意してください。さて、 SelectObject は戻り値に直前に指定されていたペンオブジェクトを返します。これはちゃんと保存をしておいてください。作成したペンオブジェクトは開放した時点で無効になってしまいますので、直前のペンオブジェクトを元に戻す必要があるのです。必ずこのプロセスは忘れないでください。さて、ペンオブジェクト(GDI オブジェクト)を開放するには、DeleteObject 関数をつかいます。作成したものは、必ず開放するようにしてください。でないとメモリリークの原因になります。

RGB マクロは COLORREF(32ビット) タイプの色値を返します。

目次へ


ブラシオブジェクトを使ってみる

さて次はブラシオブジェクトをつかってみましょう。ブラシは塗りつぶしにつかいます。早速例題を見てみましょう。

/********************************************************************************

* メッセージ以外は前回の例題と同じなので省略

********************************************************************************/

// メッセージ処理

void OnLButtonDown(HWND hWnd, POINT p)

{

HDC hDC = NULL;

HBRUSH hBrush = NULL, hOldBrush = NULL;

HPEN hPen = NULL, hOldPen = NULL;

POINT cp[3];

cp[0].x = p.x - 30 - 20;

cp[0].y = p.y - 30 + 20;

cp[1].x = p.x - 30 + 20;

cp[1].y = p.y - 30 + 20;

cp[2].x = p.x - 30;

cp[2].y = p.y - 30 - 20;

hDC = GetDC(hWnd);

hPen = CreatePen(PS_SOLID, 1, RGB(200, 255, 200));

hOldPen = (HPEN)SelectObject(hDC, hPen);

hBrush = CreateSolidBrush(RGB(0, 255, 200));

hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);

Polygon(hDC, cp, 3);

hBrush = CreateHatchBrush(HS_DIAGCROSS, RGB(255, 0, 0));

DeleteObject( SelectObject(hDC, hBrush) );

Ellipse(hDC, p.x + 30, p.y + 30, p.x + 60, p.y + 60);

SelectObject(hDC, hOldBrush);

SelectObject(hDC, hOldPen);

DeleteObject(hBrush);

DeleteObject(hPen);

ReleaseDC(hWnd, hDC);

}

ブラシの作成には、 CreateSolidBrush CreateHatchBrush 関数をつかっています。実行してみたら分かるように、 CreateSolidBrush で作成したブラシは塗りつぶしを行います。 CreateHatchBrush で作成したブラシは斜線(ハッチ)の入った塗りつぶしになります。ハッチパターにはこの他いろいろありますので、自分で確認しておいてください。また、ここでは多角形を描くのに Polygon 関数をつかっています。今回は三角形を描きましたが、頂点数を増やせば、5角形でも6角形でも描けます。

このような感じになります。

目次へ


フォントオブジェクトを使ってみる

それでは次はフォントを扱ってみましょう。ウィンドウズには標準で付属している「MS ゴシック」をつかってみます。

/********************************************************************************

* メッセージ処理以外は前回例題と同じ

********************************************************************************/

#include

// メッセージ処理

void OnLButtonDown(HWND hWnd, POINT p)

{

HFONT hFont = NULL, hOldFont = NULL;

HDC hDC = NULL;

char lpStr[80];

int red, green, blue;

red = rand() % 256;

green = rand() % 256;

blue = rand() % 256;

hDC = GetDC(hWnd);

hFont = CreateFont(24, 0, 450, 450, FW_NORMAL, FALSE, FALSE, FALSE, SHIFTJIS_CHARSET,

OUT_CHARACTER_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,

DEFAULT_PITCH, "MS ゴシック");

hOldFont = (HFONT)SelectObject(hDC, hFont);

SetTextColor(hDC, RGB(red, green, blue));

lstrcpy(lpStr, "テストです");

SetBkMode(hDC, TRANSPARENT);

TextOut(hDC, p.x, p.y, lpStr, lstrlen(lpStr));

SelectObject(hDC, hOldFont);

DeleteObject(hFont);

ReleaseDC(hWnd, hDC);

}

フォントオブジェクトの作成には CreateFont 関数をつかいます。パラメータの数なんと13!で、やたら多いのですが MSDN ライブラリを見れば、多いだけで大して問題はないことに気づくでしょう。もしかしたらCreateFontIndirect 関数で、構造体をつかったほうがいいかもしれません。フォントの色の指定には SetTextColor 関数をつかいます。背景モードの指定には SetBkMode 関数をつかいます。この関数をつかうことで、テキストの背景を透明にしたり、色をつけたりすることが出来ます。テキストの出力には TextOut 関数をつかいます。そのほかテキストを出力する関数には DrawText / DrawTextEx 関数をつかいます。

このような感じになります。

(注1) ウィンドウズプログラミングでは、文字列操作関数は strlen strcpy をつかう代りに lstrlen lstrcpy をつかいます。これは、sprintf をつかわず wsprintf をつかう理由と同じです。 lstrlen / lstrcpy / wsprintf はウィンドウズのライブラリに定義されています。ここでもし標準関数をつかってしまうと、実行プログラムに標準関数のライブラリをつむことになるので、実行ファイルがやや大きくなってしまいます。できるだけ lstrlen wsprintf をつかうようにしましょう。

2002年追記  と思いきや、VC++の場合は、既に標準ライブラリを積んでいるようです。というのも、VC++のライブラリは初期化の時点で標準ライブラリの初期化も行っているからです。

(注2)フォント名で「MS ゴシック」となっていますが、「MS」と「ゴシック」は全角文字、その間の空白は半角なので気をつけてください。

目次へ


ビットマップを使ってみる

次はビットマップを扱ってみましょう。結構簡単に扱えます。そのまえに、何でもいいですからビットマップ形式のファイルを作っておいてください。めんどくさい方は、ここから取っていってください。

画像を落としたら、きちんとビットマップに変換して、プロジェクトのフォルダの中に加えてください。

/********************************************************************************

* メッセージ処理ビットマップの使用

* メッセージ処理以外は前回例題と同じ

********************************************************************************/

#define BMP_FILE_NAME "neko.bmp"

// メッセージ処理

void OnLButtonDown(HWND hWnd, POINT p)

{

HDC hDC = NULL, hDCBitmap = NULL;

HBITMAP hBitmap = NULL;

hDC = GetDC(hWnd);

hDCBitmap = CreateCompatibleDC(hDC);

hBitmap = (HBITMAP)LoadImage(NULL, BMP_FILE_NAME, IMAGE_BITMAP,

179, 109, LR_LOADFROMFILE|LR_CREATEDIBSECTION);

SelectObject(hDCBitmap, hBitmap);

BitBlt(hDC, p.x, p.y, 179, 109, hDCBitmap, 0, 0, SRCCOPY);

DeleteDC(hDCBitmap);

DeleteObject(hBitmap);

ReleaseDC(hWnd, hDC);

}

ビットマップオブジェクトの作成には、 LoadImage 関数をつかいます。実際コピーをする場合には、ビットマップのメモリデバイスコンテキストを割り当ててやる必要があります。そのため、メモリデバイスコンテキストをCreateCompatibleDC で作成し、このデバイスコンテキストにビットマップオブジェクトを割り当てています。メモリ転送には BitBlt 関数をつかっています。CreateCompatibleDC 関数でデバイスコンテキストを作成した場合、このデバイスコンテキストを開放するには、 ReleaseDC 関数ではなく DeleteDC 関数をつかいます。

このような感じになります。

デバイスコンテキストの中にもいくつかの種類があって、メモリデバイスコンテキストもいくつかあるデバイスコンテキストのひとつです。メモリデバイスコンテキストは、ビットマップに書き込みを行うことが出来るデバイスコンテキストのことです。ちなみに今までわたし達が画面の描画につかってきたデバイスコンテキストを、ディスプレイデバイスコンテキストといいます。そのほかには、プリンタデバイスコンテキストなどがあります。

ここの冒頭でもいったように、デバイスコンテキストは絵を描くことにたとえたらスケッチブックみたいなものです。そのスケッチブックが、プリンタから出力されたり、ディスプレイに描画されたり、ビットマップが描かれていたりするわけです。

目次へ


描画メソッド

描画メソッドについては、あまり説明しないまま今までつかってきました。 LineTo 関数や Ellipse 関数などです。少数のメソッドしか登場していないので、ここでほかの描画メソッドについても紹介しておきましょう。

描画メソッドには、塗りつぶし描画メソッド曲線描画メソッドがあります。塗りつぶし描画メソッドは、文字のとおり塗りつぶしを行います。曲線描画メソッドは、塗りつぶしを行わずに、曲線のみを描画します。塗りつぶしはブラシで設定します。デバイスコンテキストで設定してあるブラシで塗りつぶしを行います。

塗りつぶし描画メソッド

メソッド名

説明

Chord

弓形を描画します。

Ellipse

楕円(円)を描画します。

FillRect

長方形を描画します。

FrameRect

長方形の境界線を指定されたブラシで描画します。

InvertRect

指定された長方形の内部を反転します。

Pie

扇形を描画します。

Polygon

多角形を描画します。

PolyPolygon

連続した多角形を描画します。

Rectangle

長方形を描画します。 FillRect と違って枠をペンで内部をブラシで描画します。

RoundRect

角の丸い長方形を描画します。

曲線描画メソッド

メソッド名

説明

AngleArc

中心から円弧の開始ポイントまで直線を描き、円弧を描画します。

Arc

楕円弧を描画します。カレントポイントを使用しません。

ArcTo

楕円弧を描画します。カレントポイントを使用します。

GetArcDirection

円弧の描画方向を取得します。

LineTo

カレントポイントから指定されたポイントまで直線を描画します。

MoveToEx

カレントポイントを指定されたポイントまで移動します。

PolyBezier

ひとつ以上のベジェ曲線を描画します。カレントポイントを使用しません。

PolyBezierTo

ひとつ以上のベジェ曲線を描画します。カレントポイントを使用します。

PolyDraw

直線とベジェ曲線をまとめて描画します。

PolyLine

連続した線分を描画します。カレントポイントを使用しません。

PolyLineTo

連続した線分を描画します。カレントポイントを使用します。

PolyPolyLine

複数の連続した線分を描画します。

SetArcDirection

円弧の描画方向を設定します。

用語

解説

(注1)カレントポイント

OS内部で保存している描画開始ポイント。

(注2)円弧の描画方向

時計回りか半時計回りか。

この他に、関数は山のように出てきますが、ここでは割愛させていただきます。だいたい「あったらいいな」と思うような機能の関数はそろってますので、じぶんで MSDN をあさって探してみてください。

Windows GDI 関数以外に、いくつか描画を扱うライブラリがあります。 GDIではハードウェアの性能をフル活用するような高速に表示を行うようなものには向きません。こういう場合は、 DirectX OpenGL などを使います。DirectX は主にゲームの描画処理に使われ、OpenGL は主に CAD 関係に使われます。どちらのライブラリも 3D 処理をすることができます。