EternalWindows
GDI / 直線

今回は、直線の描画について説明します。 直線を描画するにはLineToを呼び出します。

BOOL LineTo(
  HDC hdc,
  int nXEnd,
  int nYEnd
);

hdcは、デバイスコンテキストのハンドルを指定します。 nXEndは、直線の終点のx座標を指定します。 nYEndは、直線の終点のy座標を指定します。

LineToの引数をよく見ると分かることですが、 この関数には直線の始点を指定する引数がありません。 これは一体どういうことなのかと思われるかもしれませんが、 実はこの関数はデバイスのコンテキストの属性の1つである カレントポジションの値を始点とするよう設計されています。 カレントポジションは、MoveToExで設定することができます。

BOOL MoveToEx(
  HDC hdc,
  int X,
  int Y,
  LPPOINT lpPoint
);

hdcは、デバイスコンテキストのハンドルを指定します。 Xは、新しい現在の位置のx座標を指定します。 Yは、新しい現在の位置のy座標を指定します。 lpPointは、以前のカレントポジションを受け取るための引数で、 受け取る必要がない場合はNULLを指定しても構いません。 ちなみに、カレントポジションの初期値は(0, 0)であり、 カレントポジションのみを取得したいような場合は、 GetCurrentPositionExを呼び出すとよいでしょう。

では、ここでMoveToExとLineToによる直線の描画を見てみましょう。

MoveToEx(hdc, 30, 30, NULL);
LineTo(hdc, 100, 100);

まず、MoveToExでカレントポジションを(30, 30)に設定します。 LineToは、カレントポジションの影響を受けるので、それが始点として働き、 (30, 30)から(99, 99)の座標に直線が描画されることになります。 LineToは終点を考慮しないので、(100, 100)の座標は含まれないことに注意してください。

今回のプログラムは、MoveToExとLineToで直線を描画します。 また、その効果を少しでも見栄えよくするためペンを作成して選択しています。

#include <windows.h>

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 HPEN hpen = NULL;

	switch (uMsg) {

	case WM_CREATE:
		hpen = CreatePen(PS_SOLID, 3, RGB(255, 0, 0));
		return hpen != NULL ? 0 : -1;

	case WM_PAINT: {
		HDC         hdc;
		HPEN        hpenPrev;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);
		
		hpenPrev = (HPEN)SelectObject(hdc, hpen);

		MoveToEx(hdc, 30, 30, NULL);
		LineTo(hdc, 100, 100);

		LineTo(hdc, 160, 300);

		SelectObject(hdc, hpenPrev);

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		if (hpen != NULL)
			DeleteObject(hpen);

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

LineToにはカレントポジションを参照するという性質の他に、 関数で指定した終点を新しいカレントポジションにするという性質も併せ持っています。 2回目のLineToの呼び出しでは1回目のLineToでカレントポジションが (100, 100)に更新されたため、(100, 100)から(160, 300)の座標に直線が描画されます。 これはつまり、直線を連続で描画したいような場合は、 以下のようなコードを書く必要がないことを意味しています。

MoveToEx(hdc, 100, 100, NULL); // 1回目のLineToで(100, 100)になっているため不要
LineTo(hdc, 160, 300);

しかし、どちらにしろLineToの最初の呼び出しではMoveToExを呼ぶ必要があるため、 LineToが始点を要求する引数を持っていないのは厄介と言わざる得ないところです。 そもそも、何故このような設計になっているのかに疑問が生じるところですが、 このような設計、つまりデバイスコンテキストの属性を参照するという設計は、 MoveToExやLineToに限ったことではないのです。

SetTextColor(hdc, RGB(255, 0, 0));
TextOut(hdc, 0, 30, szText, lstrlen(szText));

これは、以前にも示したテキストの色を変更して描画するコードですが、 このSetTextColorとMoveToExが行っていることは、原理的には全く同じです。 どちらも、デバイスコンテキストの属性を変更しているに過ぎません。 そして、TextOutとLineToが行っていることも原理的には同じです。 どちらも、デバイスコンテキストの属性を参照して描画を行っているだけです。 つまり、本質的に考えるとMoveToExやLineToの挙動は、 GDIの描画関数として全く問題はないのです。 ただ、属性と引数の分別が特に目立ったケースであるというだけのことです。


戻る