본문 바로가기
Win32 API/기초

Win32 API 기초 : 핸들, DC, 윈도우 이벤트 (2)

글: 시플마 2024. 6. 17.

저번에는 기본 펜과 브러쉬를 통해 

검은색 테두리를 가진 흰색 사각형을 만들었습니다.

 

이번에는 펜과 브러쉬의 색을 지정하여 사각형을 출력해 보죠.

 

WndProc 함수에서 

 

WM_PAINT case를 보시면

150번째 줄에 CreatePen 함수가 보입니다.

 

함수명 그대로 펜을 만드는 함수인데요, 

 

인자로 세 개를 받습니다. 

각 인자가 무엇을 의미하는지 검색하여 살펴보죠.

 

먼저 첫 번째 인자인 iStyle은

 

펜의 스타일을 의미합니다.

 

파선, 점선 등 다양한 스타일이 존재합니다.

 

 

두 번째 인자인 cWidth는

 

펜의 너비를 의미합니다.

 

인자로 넣을 때 입력하는 숫자는

픽셀 단위의 단위를 의미합니다.

 

펜의 두께라고 생각하시면 되겠습니다.

 

 

세 번째 인자인 color는

 

펜의 색을 의미합니다.

 

RGB 매크로를 사용하여 색을 지정할 수 있죠.

 

 

WM_PAINT case 안에 있는 150번째 줄에 의해 

펜의 형태가 실선이며 너비는 1픽셀이고 색은 빨간색인

펜이 만들어졌음을 알 수 있죠.

 

펜의 색을 지정해 주는 RGB 매크로에 대해서 더 알아보죠.

 

RGB 매크로를 통해 세 개의 값을 넣으면 

그 값을 통해 오른쪽 코드를 실행하는 것입니다.

 

오른쪽 코드에서 BYTE는

 

unsigned char, 즉 0 ~ 255까지 표현 가능한 정수이죠.

 

만약 RGB 매크로를 통해 255, 30, 0을 넣으면 

 

위와 같이 치환되어 코드가 실행되겠죠.

 

색을 표현할 때 총 3Byte를 1Byte씩 할당하여 R, G, B를 표현하다고 했죠?

 

이때 가장 오른쪽 비트가 R입니다.

 

우선 R에 해당하는 1Byte에 255를 넣습니다.

 

그리고 G에 해당하는 위치로 이동하기 위해

왼쪽으로 8비트 시프트 연산을 한 후, 30을 넣습니다. 

 

 

그 다음 비트 합 연산( | )을 통해 

 

R과 G의 비트를 합칩니다.

 

이후B에 해당하는 위치로 이동하기 위해

왼쪽으로 16비트 시프트 연산을 한 후, 0을 넣고 

비트 합 연산( | )을 통해 이전 비트들과 합칩니다.

 

이렇게 하여 픽셀 한 칸의 색을 표현하게 되는 것이죠.

 

 

 


 

 

 

151번째 줄을 봅시다. 

 

빨간색 펜을 만들었으니 이제 이것을 적용시켜야겠죠?

 

기존의 검은펜으르 갖고 있던 hdc가

새로 만든 빨간펜을 사용하도록 SelectObject 함수를 호출합니다.

 

SelectObject 함수의 첫 번째 인자가

두 번째 인자를 선택하도록 하는 것이죠.

 

그 다음 hdc가 기존에 갖고 있던 검은펜을

HPEN형 객체 defaultPen에 저장해 둡니다.

 

저장하기 전, SelectObject 함수가 반환하는 것을

HPEN으로 형변환해 주는 이유는 SelectObject 함수가 반환하는 것이

 

HGDIOBJ형이기 때문입니다.

 

HGDIOBJ는 void 포인터로

 

HPEN형인 defaultPen에 대입하려면 

HPEN으로 형변환하여 자료형을 같게 해야 하는 것이죠.

 

 

SelectObject 함수의 반환형이 void 포인터인 이유는

대입하기 전에 대입하고자 하는 변수와 자료형만 같게 해 준다면

다양한 상황에서 SelectObject 함수를 적용할 수 있기 때문이죠.

 

SelectObject 함수의 범용성을 위해서라고 보면 되겠네요.

 

 


 

펜의 색을 지정한 후 WM_PAINT case에서 153번째 줄에 있는 

 

Rectangle 함수로 인해 빨간색 펜으로 그려진 사각형이 출력되었습니다.

 

 

빨간색 펜으로 사각형을 그리고 나면 

155번째 줄의 코드를 통해 다시 hdc가 기존 검은색 펜을 선택하게 하고 

DeleteObject 함수를 통해 사용이 끝난 빨간색 펜을 제거합니다.

 

 

 


 

 

 

GetStockObject 함수가 존재하는데,

이는 자주 쓰는 GDI Object(그래픽 출력에 사용되는 오브젝트)는

미리 만들어져 있고 이렇게 미리 만들어진 오브젝트를

사용할 수 있도록 하는 함수입니다.

 

검은색 브러시, 흰색 펜 등 자주 사용할 거 같은 건

굳이 만들 필요 없이 GetStockObject 함수를 통해

바로 사용할 수 있죠.

 

심지어 직접 만들면 DeleteObject 함수를 통해

제거도 해 주어야 하지만 GetStockObject 함수를 통해

사용한 오브젝트는 삭제해 주지 않아도 되죠.

 

 

하지만 파란색 브러시는 없으니 브러시도 직접 만들어서 해 보죠.

 

브러시도 만들고, 사용하고, 제거하는 과정이 

펜과 크게 다르지는 않지만 

 

펜과는 다르게 인자로 옵션을 주지 않고 

함수가 따로 존재합니다.

 

그래서 인자로 색만 지정해 주면 되죠.

 

파란색 브러시를 만들 것이기 때문에 R과 G의 값은 0으로

B의 값을 255로 하겠습니다.

 

 

그리고 154번 째 줄에서 hdc가 파란색 브러시를 

선택하도록 하고 hdc가 기존에 갖고 있던 흰색 브러시를

defaultBrush에 저장합니다.

 

그 다음 파란색 브러시로 칠해진 사각형을 출력하고

hdc가 다시 흰색 브러시를 선택하게 한 후,

사용이 끝난 파란색 브러시를 제거합니다.

 

 


 

 

 

윈도우에 클릭 메시지를 발생시키고 

좌푯값을 저장해 봅시다.

 

클릭한 곳의 x 좌푯값을 저장할 g_x와

클릭한 곳의 y 좌푯값을 저장할 g_y를 선언합니다.

 

 

그리고

 

WndProc 함수에 클릭 시 전역 변수에 

클릭한 곳의 좌푯값이 저장될 수 있도록

WM_LBUTTONDOWN case를 추가합니다.

 

WM_LBUTTONDOWN은 좌클릭을 하는 순간

작동합니다. 좌클릭 진행 시 좌푯값은 

 

WndProc 함수의 인자 중 lParam이 담당합니다.

 

윈도우의 어딘가를 클릭하면 그곳의 좌푯값이 

lParam에 저장되는 것이죠.

 

 

그런데 좌표는 x와 y로 구성되어 있는데 

인자로 들어오는 값은 lParam 하나입니다.

 

어떻게 이 값을 x와 y로 구분할 수 있을까요?

 

이를 알기 위해서 자료형부터 살펴봐야 합니다.

 

lParam의 자료형은 LPARAM입니다.

 

LPARAM은 LONG_PTR을 의미합니다.

 

다시 LONG_PTR이 무엇인지 확인해 보니

long이네요.  4Byte 정수형이죠.

 

lParam은 앞 2Byte를 y 좌푯값으로

 

뒤 2Byte를 x 좌푯값으로 봅니다.

 

이렇게 lParam을 x와 y 좌표로 분리해 주는

매크로가 LOWORD와 HIWORD입니다.

 

 

LOWORD와 HIWORD의 동작을 이해하기 위해서

 

프로그램을 실행하고 아무 데나 좌클릭을 진행하였습니다.

 

그러자 lParam에 65540이라는 값이 들어왔습니다.

 

이를 프로그래머 계산기를 이용해 비트로 나타내 봅시다.

 

65540이라는 값은 BIN(Binary)에서 확인할 수 있듯이 

0001 0000 0000 0000 0100 으로 나타낼 수 있네요.

 

lParam은 long형(4Byte)이므로 조금 더 보기 편하게 표현하면 

0000 0000 0000 0001 0000 0000 0000 0100 입니다.

 

빨간 부분과 파란 부분이 각 2Byte(16Bit)로

총 4Byte(32Bit)죠.

 

이 상태에서 LOWORD 동작부터 살펴봅시다.

 

lParam을 우선 DWORD_PTR로 형변환합니다.

 

DWORD_PTR은 ULONG_PTR이고

 

ULONG_PTR은 unsigned long이네요.

 

양수만 표현하는 4Byte 정수입니다.

 

어차피 lParam이 4Byte 정수이므로 형변환을 한다고 하여도

큰 변화는 없을 겁니다.

 

이후 0xffff와 & 연산을 진행합니다.

 

0xffff는 비트로 표현하면 

1111 1111 1111 1111 입니다.

 

& 연산은 두 비트를 비교하여

두 비트 모두 1이면 1을 반환합니다.

 

0000 0000 0000 0001 0000 0000 0000 0100

1111 1111 1111 1111 를 & 연산하면 

 

그대로겠죠?

 

이 상태에서 해당 비트들을 WORD로 형변환합니다.

 

WORD는 unsiged short형으로

 

양수만을 표현하는 2Byte 정수죠.

 

근데 0000 0000 0000 0001 0000 0000 0000 0100 은 

4Byte입니다. 이를 형변환하면 상위 2Byte가 사라지며

파란 부분인 0000 0000 0000 0100 만 남게 되겠죠.

남은 부분이 x 좌푯값을 저장하기 위한 전역 변수 g_x에 대입됩니다.

 

0000 0000 0000 0100 은 정수로 표현하면 4죠.

 

확인해 보니 정말 g_x에 4가 대입되었습니다.

 

 

이번에는 HIWORD 동작을 살펴봅시다.

 

마찬가지로 lParam을 DWORD_PTR로 형변환한 후

오른쪽으로 비트 시프트 연산을 진행합니다.

 

0000 0000 0000 0001 0000 0000 0000 0100 에서

오른쪽으로 16 비트 연산을 진행하면

빨간 부분인 0000 0000 0000 0001 만 남게 되겠죠.

 

그 다음 & 연산을 진행하고 해당 비트들을 WORD로 형변환합니다.

 

이를 y 좌푯값을 저장하기 위한 전역 변수 g_y에 대입합니다.

 

0000 0000 0000 0001 을 정수로 표현하면 1이죠.

 

확인해 보니 정말 g_y에 1이 대입되었습니다.

 

 

 


 

 

 

이번에는 사각형을 출력하고 

키를 입력하여 출력된 사각형을 움직이도록 해 보죠.

 

이를 위해서 사각형이 출력될 위치를 저장하는 변수가 필요할 겁니다.

 

고정된 위치 값이 아닌 변수에 위치를 저장해야

키 입력에 따라 변수의 값을 계속 바꿀 수 있고

이를 갱신해 주면 사각형이 출력되는 위치가 바뀌겠죠?

 

사각형의 크기를 나타내는 변수 g_recObjectScale과 

 

사각형 중심의 좌표를 나타내는 변수 g_objectPos를 선언하였습니다.

 

여기서 POINT형은 

 

long형 변수 두 개를 갖고 있는 구조체입니다.

 

구조체 형태를 보면 C 스타일로 되어 있는데 

C와 C++에서 모두 사용할 수 있겠네요.

 

 

WndProc 함수에서 

 

사각형을 출력하는 Rectangle 함수의 인자를 수정했습니다.

 

위에서 선언한 전역 변수를 활용했죠.

 

 Rectangle 함수의 left 인자로

사각형의 중심으로부터 출력하고자 하는 

사각형 크기의 절반(50)에 해당하는 값을 빼 넣습니다.

 

그리고  Rectangle 함수의 top 인자로

사각형의 중심으로부터 출력하고자 하는 

사각형 크기의 절반(50)에 해당하는 값을 빼 넣습니다.

 

중심 위치로부터 빼는 이유는 

사각형의 왼쪽 상단을 구성하는 부분이기 때문입니다.

 

 Rectangle 함수의 right 인자로

사각형의 중심으로부터 출력하고자 하는 

사각형 크기의 절반(50)에 해당하는 값을 더하여 넣습니다.

 

그리고  Rectangle 함수의 bottom 인자로

사각형의 중심으로부터 출력하고자 하는 

사각형 크기의 절반(50)에 해당하는 값을 더하여 넣습니다.

 

중심 위치로부터 더하는 이유는 

사각형의 오른쪽 하단을 구성하는 부분이기 때문입니다.

 

 

이제 키 입력을 받을 수 있게 

 

WM_KEYDOWN case를 추가했습니다.

 

키 입력이 들어오면 

WM_KEYDOWN case 안의 코드를 실행하게 됩니다.

 

키 입력 중에서 VK_UP(위쪽 방향키) 입력부터 작성하겠습니다.

위쪽 방향키를 입력하면 사각형이 위로 올라가야 하므로

사각형의 중심 좌표 중 y 값을 빼면 되겠죠.

 

이후 프로그램을 실행하고 위쪽 방향키를 눌렀는데

 

사각형이 움직이지 않습니다.

 

여기서 윈도우를 내렸다가 다시 올렸더니

 

그제서야 사각형이 위로 움직인 모습입니다.

 

이유는 위쪽 방향키를 누름으로써 사각형의 중심 좌푯값은 바뀌었겠지만

바뀐 값을 토대로 사각형을 다시 그리지 않았기 때문에

위치가 바뀌기 전 사각형이 계속 보이는 것입니다.

 

그 상태에서 윈도우를 내렸다가 다시 올리면

무효화 영역이 발생했다고 인지하여

WM_PAINT case가 실행되고 바뀐 중심값을 기준으로

사각형이 다시 그려져 움직인 것처럼 보이는 것이죠.

 

 

이를 해결하기 위해서는 키 입력이 들어오면

바로 무효화 영역이 발생했다고 알려주면 되겠죠.

 

이를 위한 함수가 바로 InvalidateRect 함수입니다.

 

첫 번째 인자로 윈도우를 넘기고, 

두 번째 인자는 영역의 범위를 의미하는데

nullptr을 넣으면 전체 영역을 업데이트합니다.

 

InvalidateRect 함수까지 추가하여 프로그램을 실행하여

위쪽 방향키를 눌렀는데 사각형의 움직임이 바로 보이나,

이전에 출력된 사각형이 지워지지 않아 화면에 그대로 남습니다.

 

이러한 현상을 없애기 위해서는 InvalidateRect 함수의

세 번째 인자를 true로 주어야 합니다.

 

세 번째 인자는 업데이트 영역을 처리할 때 업데이트 이전에 있던

배경을 지울지 말지 여부를 결정하는 역할이죠.

 

잘 작동하는지 확인해 보겠습니다.

 

잔상이 남지 않고 사각형이 잘 움직이는 모습이네요.

 

 

다양한 키 입력을 인식할 수 있도록 매크로가 많이 지정되어 있는데

 

이를 바탕으로 다른 키 입력도 추가하겠습니다.

 

아래, 왼쪽, 오른쪽 방향키 입력에 대한 처리도 작성을 했습니다.

 

 

또한 W, A, S, D와 같이 문자를 통해 키 입력을 받을 수도 있습니다.

 

이 경우 반드시 대문자로

case 문을 작성해야 인식합니다. 

 

 

키 입력을 받아서 사각형을 움직일 때

원하는 방향키를 쭉 누르고 있으면 해당 방향으로 쭉 

움직이기는 하나 처음에 한 박자 끊기는 느낌이 듭니다. 

 

이유는 처음 키 입력을 받고 사각형이 한 번 움직였다가

1초 정도 계속 입력이 들어오면 그때서야 쭉 움직이기 시작하기 때문이죠.

 

개선해야 하는 부분 중 하나입니다.

 

 


강의 출처 : https://www.youtube.com/watch?v=RDJ-yCvT9DM