본문 바로가기
핵심 API로 배우는 윈도우 프로그래밍

[windowAPI] 마우스 클릭&드래그로 고무줄 직선 그리기

by 딴짓거리 2022. 9. 24.

핵심 API로 배우는 윈도우 프로그래밍 3장 중 레스터 연산을 공부하고 정리한 내용입니다.

 

우리는 이제까지 화면에 움직이는 무언가를 출력 할 때 화면 전체를 초기화 하는 방식으로 프로그래밍 했었다.

 

하지만 이 방법은 한번에 두개 이상의 오브젝트를 제어하기 매우 불편하고

 

무엇보다 화면이 깜빡깜빡 어지럽다는 문제점을 가지고 있다.

 

그래서 나온게 레스터 연산이다.

 

hdc = GetDC(hwnd);
SetROP2(hdc, 상수);

SetROP2 함수는 쉽게 말하면 "나 이제부터 (상수)의 방법으로 hdc화면에 그림그릴꺼에요" 라고 말해주는 함수이다.

그러므로 GetDC() 를통한 현재 화면의 정보를 받아서 인자로 전달해주어야 한다.

 

우리가 살펴볼 XOR레스터 연산을 위해서는 "상수"에 R2_XORPEN을 넣어주면 된다.

 

XOR 연산이란

A B A ^ B
0 0 0
0 1 1
1 0 1
1 1 0

쉽게말해 A와 B가 같으면 TRUE 다르면 FALSE 이다.

 

이를 이용해 그림을 그리면 전체 화면을 초기화 시킬 필요 없이 원하는 부분만 수정할 수 있어 매우 효율적이다.

 

 

이를 이용해 마우스 포인터 끝을 따라 늘어나는 직선을 그려보자

 

책에서는 시작점이 지정되어있지만 그러면 재미가 없으므로

 

내가 클릭을 시작한 부분을 시작점으로 설정해서 좀더 자유롭게 그릴 수 있게 코드를 바꿔보았다.

 

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <TCHAR.H>
#include <math.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)
{
	HWND		hwnd;
	MSG			msg;
	WNDCLASS	WndClass;
	WndClass.style = CS_HREDRAW | CS_VREDRAW;
	WndClass.lpfnWndProc = WndProc;
	WndClass.cbClsExtra = 0;
	WndClass.cbWndExtra = 0;
	WndClass.hInstance = hInstance;
	WndClass.hIcon = LoadIcon(NULL, IDI_QUESTION);		//윈도우 아이콘
	WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);		//커서 모양
	WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);	//배경 색
	WndClass.lpszMenuName = NULL;	//메뉴 이름
	WndClass.lpszClassName = _T("Window Class Name");	//윈도우 클래스 이름
	RegisterClass(&WndClass);
	hwnd = CreateWindow(_T("Window Class Name"),
		_T("Cobaltbru's First Window"),		//윈도우 타이틀 이름
		WS_OVERLAPPEDWINDOW,		//윈도우 스타일
		200,	//윈도우 위치 X
		300,	//윈도우 위치 Y
		600,	//윈도우 가로
		400,	//윈도우 세로
		NULL,
		NULL,
		hInstance,
		NULL
	);
	ShowWindow(hwnd, nCmdShow);		//윈도우 기본 출력 함수
	UpdateWindow(hwnd);
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
	HDC			hdc;
	PAINTSTRUCT	ps;
	static int	startX, startY, oldX, oldY;
	static BOOL	Drag;
	int			endX, endY;

	switch (iMsg)
	{
	case WM_CREATE:
		Drag = FALSE;
		break;
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);
		EndPaint(hwnd, &ps);
		break;
	case WM_LBUTTONDOWN:
		startX = oldX = LOWORD(lParam);
		startY = oldY = HIWORD(lParam);
		Drag = TRUE;
		break;
	case WM_LBUTTONUP:
		//startX = oldX = 0;
		//startY = oldY = 0;
		Drag = FALSE;
		InvalidateRgn(hwnd, NULL, TRUE);
		break;
	case WM_MOUSEMOVE:
		hdc = GetDC(hwnd);
		if (Drag)
		{
			SetROP2(hdc, R2_XORPEN);
			SelectObject(hdc, (HPEN)GetStockObject(WHITE_PEN));
			endX = LOWORD(lParam);
			endY = HIWORD(lParam);
			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, oldX, oldY);
			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, endX, endY);
			oldX = endX; oldY = endY;
		}
		ReleaseDC(hwnd, hdc);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}
	return(DefWindowProc(hwnd, iMsg, wParam, lParam));
	


}

차근차근 살펴보자!

 

HDC			hdc;
	PAINTSTRUCT	ps;
	static int	startX, startY, oldX, oldY;
	static BOOL	Drag;
	int			endX, endY;

oldX, Y가 뭐냐하면

 

레스터 연산으로 기존의 직선을 지워주려면 전에 그렸던 직선을 똑같이 한번 덮어 씌워줘야 하기때문에

old X,Y로 직전 직선의 끝점을 저장해 주었다. (드래그 한채로 움직이면 시작점은 전과 후가 같다)

그리고 마우스가 화면을 드래그(왼쪽 키를 누른채로 이동)해야 직선이 그려져야 하므로

Drag 부울값도 하나 만든다.

 

case WM_LBUTTONDOWN:
		startX = oldX = LOWORD(lParam);
		startY = oldY = HIWORD(lParam);
		Drag = TRUE;
		break;

마우스 왼쪽 버튼이 눌리면

startX,Y oldX,Y가 전부 현재 마우스 좌표로 초기화 된다.

이러면 직선의 시작과 끝좌표가 같기 때문에 일단은 화면에 직선이 나타나지 않게 된다.

즉 마우스를 새로 누를때마다 화면은 깨끗해진다.

또한 Drag값을 TRUE로 바꿈으로써 드래그중이라고 알려준다.

 

case WM_MOUSEMOVE:
		hdc = GetDC(hwnd);
		if (Drag)
		{
			SetROP2(hdc, R2_XORPEN);
			SelectObject(hdc, (HPEN)GetStockObject(WHITE_PEN));
			endX = LOWORD(lParam);
			endY = HIWORD(lParam);
			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, oldX, oldY);
			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, endX, endY);
			oldX = endX; oldY = endY;
		}
		ReleaseDC(hwnd, hdc);
		break;

마우스가 움직인다면 지금 드래그중인지 체크하고

드래그중이라면 본격적으로 직선을 그리기 시작한다.

먼저 hdc 화면에서는 레스터 연산을 할 것이라고 함수를 선언해준다.

 

그리고 WHITE_PEN으로 하얀색 펜을 선택한다.

하얀 배경(1)과 하얀 펜(1)이 XOR연산을 해야 검은색(0) 선이 나타날 것이다.

 

직선은 처음 드래그를 시작한 위치부터 현재 마우스의 좌표까지 그어져야 하므로

endX,Y에 현재 좌표를 계속 그려준다.

 

			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, oldX, oldY);
			MoveToEx(hdc, startX, startY, NULL);
			LineTo(hdc, endX, endY);
			oldX = endX; oldY = endY;

여기서 헷갈릴 수 있는데

먼저 이전에 저장해둔 직전 끝좌표로 직선을 한번 그린다

레스터 연산으로 인해 검은선(0)과 하얀선(1)이 만나 하얀선(1)이 되어 결과적으로 이전 직선이 지워진다.

그 다음 현재 마우스 좌표로 직선을 그린다.

그리고 그 좌표를 다시 oldXY에 저장하여 다음 레스터 연산을 준비한다.

 

이로써 잔상없는 직선을 연속적으로 그릴 수 있다.

 

case WM_LBUTTONUP:
		startX = oldX = 0;
		startY = oldY = 0;
		Drag = FALSE;
		InvalidateRgn(hwnd, NULL, TRUE);
		break;

이제 버튼을 뗀다

버튼을 때면 시작과 끝점을 즉시 0으로 초기화 시켜주고

Drag를 FALSE로 설정해주고 WM_PAINT를 호출한다.

 

WM_PAINT에는 아무것도 없기때문에 빈화면을 출력한다. 결과적으로 마우스 좌클릭을 떼는순간 화면이 초기화 된다.

 

선이 얇아서인지 움짤로 잘 나타나지 않는다...

여러모로 생각할게 많았던 재미있는 예제였다.

다음예제는 모양만 다른 사실상 같은 예제이므로 포스팅은 스킵한다.