EternalWindows
GDI / 直線判定

前節で呼び出したLineToのような直線を描画する関数は、 デジタル差分解析(DDA : Digital Differential Analyzer)を使って、 直線を定義するピクセルの集合を決定しています。 DDAとは、直線上の各点を調べ、その点に対応する表示面のピクセルや印刷ページの ドットを判断する方法のことで、アプリケーションはLineDDAを通じて その各点を知ることができます。

BOOL LineDDA(
  int nXStart,
  int nYStart,
  int nXEnd,
  int nYEnd,
  LINEDDAPROC lpLineFunc,
  LPARAM lpData
);

nXStartは、直線の始点のx座標を指定します。 nYStartは、直線の始点のy座標を指定します。 nXEndは、直線の終点のx座標を指定します。 nYEndは、直線の終点のy座標を指定します。 lpLineFuncは、コールバック関数のアドレスを指定します。 この関数に直線を定義する点が次々と送られます。 lpDataは、lpLineFuncに渡したいアプリケーション定義のデータです。 lpLineFuncは、以下のようなプロトタイプを持たなければなりません。

VOID CALLBACK LineDDAProc(
  int X,
  int Y,
  LPARAM lpData
);

Xは、現在の点のx座標です。 Yは、現在の点のy座標です。 lpDataは、LineDDAのlpDataに指定した値です。 コールバック関数は、直線を定義する点の全てを考慮するまで呼ばれ続けるため、 LineDDAが制御を返すのは、全ての点を考慮した後になります。

LineDDAを呼び出すということは、いわば直線の描画アルゴリズムを取得できることです。 このアルゴリズムはLineToのような関数が利用していますから、 たとえば、コールバック関数に与えられた点を基にSetPixcelを呼び出せば、 直線を描画しているのと同じ結果が得られます。

void CALLBACK LineDDAProc(int x, int y, LPARAM lParam)
{
	HDC hdc = (HDC)lParam;

	SetPixel(hdc, x, y, RGB(0, 0, 0));
}

この例では、LineDDAの第6引数にデバイスコンテキストのハンドルを指定したものと仮定し、 それと点を構成する座標を使ってSetPixelを呼び出します。 SetPixelは、第4引数の色で指定した座標にピクセルを描画しますから、 たとえば、LineDDAを次のように呼び出したならば、

case WM_PAINT: {
	HDC         hdc;
	PAINTSTRUCT ps;

	hdc = BeginPaint(hwnd, &ps);

	LineDDA(30, 30, 100, 100, LineDDAProc, (LPARAM)hdc);

	EndPaint(hwnd, &ps);

	return 0;
}

(30, 30)から(99, 99)までの黒い直線が描画されることになります。

先の例では、LineDDAとコールバック関数の挙動を確認しただけであって、 実際には、このような使い方は推奨されるものではありません。 LineDDAは点の計算を行うがために動作が低速ですし、 SetPixelの繰り返しの呼び出しがパフォーマンスを一層低下させているため、 普通にMoveToExとLineToで直線を描画するほうが明らかに効率的です。 しかし、LineDDAが直線を定義する点を求めるという魅力的な関数であるのに 変わりありませんから、何とかしてその効果を活かす使い方を考えたいものです。

今回のプログラムは、直線上をマウスで押下したかどうかを判定します。

#include <windows.h>

struct LINE {
	int  x;
	int  y;
	BOOL bPoint;
};
typedef struct LINE LINE;
typedef struct LINE *LPLINE;

void CALLBACK LineDDAProc(int x, int y, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static POINT ptEnd = {0};
	static POINT ptBegin = {0};

	switch (uMsg) {

	case WM_CREATE:
		ptBegin.x = 30;
		ptBegin.y = 30;
		
		ptEnd.x = 130;
		ptEnd.y = 130;

		return 0;

	case WM_PAINT: {
		HDC         hdc;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);
	
		MoveToEx(hdc, ptBegin.x, ptBegin.y, NULL);
		LineTo(hdc, ptEnd.x, ptEnd.y);

		EndPaint(hwnd, &ps);

		return 0;
	}
	
	case WM_LBUTTONDOWN: {
		LINE  line;
		TCHAR szBuf[256];

		line.x = LOWORD(lParam);
		line.y = HIWORD(lParam);
		
		line.bPoint = FALSE;
		LineDDA(ptBegin.x, ptBegin.y, ptEnd.x, ptEnd.y, LineDDAProc, (LPARAM)&line);

		if (line.bPoint) {
			wsprintf(szBuf, TEXT("%d %d"), line.x, line.y);
			SetWindowText(hwnd, szBuf);
		}
		else
			SetWindowText(hwnd, TEXT("---"));

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void CALLBACK LineDDAProc(int x, int y, LPARAM lParam)
{
	LPLINE lpLine = (LPLINE)lParam;

	if (x == lpLine->x && y == lpLine->y)
		lpLine->bPoint = TRUE;
}

このプログラムは、前節のようにWM_PAINTで直線を描画するのに加え、 その座標を基にWM_LBUTTONDOWNでLineDDAを呼び出しています。 コールバック関数で行われることは、マウスの左ボタンが押下されたときの カーソルの座標が、直線を定義する点と一致するかどうかです。 つまり、直線をクリックしたかどうかを判別します。

case WM_LBUTTONDOWN: {
	LINE  line;
	TCHAR szBuf[256];

	line.x = LOWORD(lParam);
	line.y = HIWORD(lParam);
	
	line.bPoint = FALSE;
	LineDDA(ptBegin.x, ptBegin.y, ptEnd.x, ptEnd.y, LineDDAProc, (LPARAM)&line);

	if (line.bPoint) {
		wsprintf(szBuf, TEXT("%d %d"), line.x, line.y);
		SetWindowText(hwnd, szBuf);
	}
	else
		SetWindowText(hwnd, TEXT("---"));

	return 0;
}

自作のLINE構造体は、カーソルの位置と直線をクリックしたかを示すフラグで構成されています。 LineDDAを呼び出す前は、クリックしたかどうかは分からないため、 bPointをFALSEにしておき、コールバック関数でそれが変化したかを後のif文で確かめます。

void CALLBACK LineDDAProc(int x, int y, LPARAM lParam)
{
	LPLINE lpLine = (LPLINE)lParam;

	if (x == lpLine->x && y == lpLine->y)
		lpLine->bPoint = TRUE;
}

コールバック関数は、直線を定義する点を一通り考慮するまで呼ばれますから、 ここでの処理はその点とカーソルの位置をひたすら比較することになります。 一致した場合は、コールバック関数の呼び出しを中止したいところですが、 このコールバック関数は戻り値がvoid型であるため、 FALSEを返すという一般のコールバック関数で利用されるテクニックは使えません。

LineDDAにHDCを要求する引数が存在しないのは、ある意味で残念なことだと思います。 もし、HDCを要求していればLineDDAはデバイスコンテキストに選択された ペンの情報を取得できるでしょうから、DDAがペンの太さに応じて変化したかもしれません。 しかし、現在のLineDDAはペンについて一切考慮していませんから、 LineDDAを直線の判定として使うならば、デフォルトの太さの直線に限られます。


戻る