목표
Win32 API을 활용해서 화면에 문자열을 어떻게 출력하는지 알아보도록 하겠습니다.
목차 클릭하면 해당 목차로 이동합니다.
개요
이전 포스팅까지 Win32 API의 동작 원리와 진입점인 WinMain과 메세지 처리 함수인 윈도우 프로시저에 대해서 알아보았습니다. 이제, 본격적으로 Win32 API를 활용해서 프로그램을 작성해보는 시간을 갖도록 하겠습니다. 이번 포스팅에는, 문자열을 출력하고 정렬하는 과정을 진행하도록 하겠습니다.
Device Context(DC)란?
Win32에서 출력을 하기 위해서는 Device Context라는 구조체가 필요합니다. 해당 구조체는 출력에 필요한 모든 정보를 갖고 있는 데이터 구조체입니다. 이 구조체가 왜 필요한지 이해하기 위해서는 화면에 출력되기까지의 과정을 알고 있어야합니다.
윈도우는 기본적으로 세 가지 동적 연결 라이브러리(Dynamic Linked Library)로 구성되어 있습니다.
- Kernel : 메모리를 관리하고 프로그램을 실행한다.
- User : 유저 인터페이스(UI)와 윈도우를 관리한다.
- GDI : Graphic Device Interface의 약자로, 화면 처리와 그래픽 등 모든 출력 장치를 제어한다.
위 세가지 동적 연결 라이브러리로 구성되어 있습니다. 이 중 GDI는 이전 포스팅에서도 잠깐 다룬 적이 있습니다. 이에 대해 자세히 다뤄보겠습니다. 먼저, Windwos API의 특징 중 하나는 "디바이스에 비의존적"이라는 점입니다. 이를 가능하게 하는 것은 GDI와 디바이스 드라이버 덕분입니다. GDI는 모니터, 프린터와 같은 모든 출력 장치를 제어하는 인터페이스입니다. 이렇게 출력 장치를 제어하고, 그 곳에 출력하는 것을 GDI 오브젝트라고 합니다. 펜, 브러시, 비트맵, 폰트 등 화면에 출력하는 요소들을 뜻합니다. 그리고 이런 GDI 오브젝트를 모아놓은 곳이 Device Context입니다. 그렇기 때문에 Device Context가 출력에 필요한 모든 정보를 가지는 데이터 구조체라고 하는 것입니다. 어떤 느낌인지 감이 오시나요? 정리하면 다음과 같습니다.
정리
→ GDI가 출력 장치를 제어
→ GDI 모듈이 Device Context를 관리함
→ 이 Device Context는 펜, 폰트 등 화면에 출력하는 요소인 GDI Object를 갖고 있음
→ 이를 활용하여 다양한 요소를 화면에 출력
Device Context가 필요한 이유?
그렇다면 왜 Device Context를 이용해서 화면에 출력하는 과정을 거칠까요? 화면에 무언가를 출력하기 위해서 개발자가 신경써야 할 부분이 많기 때문입니다. 화면에 선을 하나 긋는다고 가정해보겠습니다. 선을 그리기 위해서는 다음과 같은 정보들이 필요합니다.
- 선의 시작점 위치
- 선의 끝점 위치
- 선의 형태 (실선, 점선 등등)
- 선의 색깔
- 선의 굵기
- 선을 그리는 모드(ROP 모드 등)
그냥 선을 하나 그으려고 하는데도 이렇게나 많은 정보들이 필요합니다. 선을 여러 줄 그리려면 더 많은 정보가 필요할 것이고, 도형이 되면 훨씬 많을 것입니다. 하지만, Device Context를 사용한 실제 API 코드는 다음과 같습니다.LineTo(hdc, X, Y); 선이 그어질 위치만 알려주면, 그 외에 정보들은 Device Context에 저장된 정보를 기반으로 자동으로 처리하는 것을 알 수 있습니다.
그 외에도, 윈도우가 실행되는 환경은 여러 개의 프로그램이 동시에 실행되는 멀티 태스킹 시스템이기 때문에, 다른 프로그램과의 상태에도 영향을 받습니다. 예를 들어, 다음과 같이 메모장 2개를 동시에 사용하고 있다고 가정해보겠습니다.
뒤에 있는 1번 메모장은 2번 메모장에 의해 일부 가려진 것을 확인할 수 있습니다. 위에 다른 프로그램이 올라온다면 해당 윈도우는 가려져야합니다. 이런 부분들도 프로그래머가 일일히 설정해줘야할까요? 당연히 아닙니다. 이와 같은 내용도 Device Context를 통해서 운영체제가 인지하여 처리할 수 있습니다. Device Context는 다른 프로그램의 윈도우끼리 출력 결과가 서로를 방해하지 않도록 완충하는 역할도 하고 있음을 알 수 있습니다.
이러한 이유들 때문에, Device Context가 필요한 것입니다.
문자열 출력하기
TextOut1 프로젝트
그럼, 바로 화면에 문자열을 출력해보도록 하겠습니다. 작성되는 코드는 이전 포스팅에서 다룬 기본 코드를 기반으로 변경된 코드만 다루겠습니다.
먼저, 실행되는 프로그램의 상단에 띄울 윈도우명을 작성합니다. 저는 위와 같이 TextOut으로 정했습니다.
윈도우 프로시저에서 사용할 변수 선언
메세지 처리 전용 함수인 WndProc 함수에 위와 같이 메세지 처리함수를 작성합니다.
HDC는 Handle Device Context의 약자입니다. DC를 다루는 핸들 구조체인 hdc를 선언했습니다. 그리고 2개의 메세지를 처리하는 것을 확인할 수 있습니다.
WM_LBUTTONDOWN 메세지
WM는 Window Message의 약자입니다. 윈도우에서 발생한 메세지인데, L BUTTON DOWN 인겁니다. 즉, 왼쪽 마우스가 눌렸을 때 발생하는 메세지입니다. DOWN이기 때문에, 눌리는 타이밍에 발생하는 메세지입니다.GetDC(hWnd) 함수를 통해 해당 윈도우의 DC를 얻을 수 있습니다. 윈도우의 DC를 얻는 함수는 GetDC, BeginPaint 2개입니다. BeginPaint 함수는 곧 나올 WM_PAINT 메세지에서만 사용할 수 있고, GetDC함수는 그 외 메세지에서 사용됩니다. 이에 대한 내용은 추후에 자세히 다루도록 하겠습니다.TextOut(DC, X좌표, Y좌표, 문자열, 문자열길이) 함수를 통해 해당 텍스트를 출력할 수 있습니다. Beautiful Korea!라는 문자열을 출력하도록 설정했습니다. 문자열 출력을 마치면 ReleaseDC를 통해 DC를 반환해야합니다. GetDC와 ReleaseDC는 세트라고 생각하면 편하고, 이는 메모리 낭비를 막기 위해 사용됩니다. 이후, return 0;를 통해 메세지 처리를 종료합니다. lParam, wParam의 정보에 따라 메세지 내부에 switch문을 또 사용하는 경우가 있는데, 이 때 break;로 구분하고 메세지 종료는 return 으로 구분합니다.
WM_DESTROY 메세지
해당 메세지는 오른쪽 위에 X표시를 누를 경우 발생하는 메세지입니다. 프로그램이 종료할 때 발생하는 메세지입니다. PostQuitMessage(0)을 통해 종료 메세지를 전송합니다.
메세지 처리가 완료된 후, 처리 결과를 다시 반환합니다.(LRESULT형)
결과
프로그램을 실행하면 아무것도 안뜨다가, 왼쪽 마우스를 클릭하면 Beautiful Korea를 출력하는 것을 볼 수 있습니다. 이 때, 문제가 하나 있습니다. 윈도우의 크기를 변경하거나 최소화했다가 다시 띄우면 문자열이 사라지는 것을 볼 수 있습니다. 윈도우가 변화하면 사라지는 것인데요. 이에 대한 원인을 알고 해결방법을 찾기 위해서 TextOut2 프로젝트를 실행해보도록 하겠습니다.
TextOut2 프로젝트
먼저 윈도우의 이름을 TextOut2로 변경하고 WndProc함수를 다음과 같이 변경합니다.
결과
먼저 결과부터 확인하면, 이번에는 프로그램을 실행하면 바로 "Beautiful Korea"가 출력되는 것을 확인할 수 있습니다. 윈도우를 최소화했다 다시 켜도, 사이즈를 바꿔도 문자열이 사라지지 않습니다. 왜 TextOut2는 문자열이 사라지지 않을까요? 이를 이해하기 위해서 처리하는 메세지를 확인해보겠습니다.
WM_PAINT 메세지
이번에는 WM_LBUTTONDOWN 메세지가 아닌, WM_PAINT 메세지입니다. WM_PAINT는 말 그대로 화면에 출력하는 메세지입니다. WM_PAINT 메세지가 발생하는 시점은 여러 개가 있습니다. 그 중에서 프로그램에 실행되었을 때도 WM_PAINT 메세지가 발생합니다. 추후에 다룰 프로그램이 시작되면 발생하는 메세지를 처리하고, 윈도우를 띄우기 위해서 WM_PAINT 메세지가 발생하는 것입니다. 그래서 윈도우를 시작하자마자 문자열이 출력되는 것입니다.근데 여기서 눈에 띄는 부분이 있죠? 바로 BeginPaint함수와 EndPaint 함수입니다. 이 함수들은 위에서 다룬 GetDC와 ReleaseDC함수와 똑같은 역할을 합니다. 둘이 어떤 차이점이 있을까요?
BeginPaint() VS GetDC()
BeginPaint()는 WM_PAINT 메세지에서만 사용하는 함수입니다. BeginPaint는 윈도우의 Clipping Region을 자동으로 파악하는 특징이 있습니다. Clipping Region이란, 클라이언트 영역의 특정 부분에 그리기를 한정하는 영역을 의미합니다. 위에서 메모장 겹치는 것을 보셨죠? 뒤에 가려져있는 메모장을 클릭하게 되면 가려져 있던 부분을 업데이트해야합니다. 이렇게 윈도우가 생성되거나, 움직이거나, 사이즈가 바뀌거나, 스크롤 되는 등 윈도우의 화면 상의변화하는 부분을 Clipping Region이라고 합니다. 일부 메세지는 화면의 변화가 아니기 때문에 화면의 변화를 감지하지 못합니다. 예를 들어, 위에서 본 TextOut 프로젝트에서 LBUTTONDOWN 메세지가 발생하면, TextOut함수를 통해서 문자열을 출력해야하는데, 마우스가 클릭된 것은 화면의 변화가 아니기 때문에 WM_PAINT 메세지가 발생하지 않습니다. 프로그램을 최소화했다가 다시 띄우면 윈도우를 그리는 WM_PAINT만 발생하고, WM_LBUTTONDOWN 메세지가 발생하지 않기 때문에 문자열이 출력되지 않는 것입니다. 그래서 우리는 화면의 변화가 있다고 윈도우에 알리기 위해 InvalidateRect() 혹은 InvalidateRgn() 함수를 사용합니다. 이에 대한 자세한 내용은 추후에 다룰 예정입니다.
GetDC()는 WM_PAINT 외에 메세지에서 출력을 하기 위해 해당 Client 영역의 Device Context를 얻는 함수입니다. TextOut 프로젝트에서 LBUTTONDOWN 메세지가 발생하면 바로 출력하는 것처럼 즉각적인 출력이 이루어집니다. 하지만, 일시적인 출력 방법으로 그 이후의 변화에는 책임지지 않습니다. 그래서, 윈도우가 최소화되었다가 띄워지거나, 윈도우의 사이즈가 변경되는 경우 문자열을 다시 출력하지 않는 것입니다. 그래서 GetDC함수는 배경과 관계없이 특정 출력을 일시적(즉각적)으로 반영하고자하는 경우 사용하곤 합니다. 여담으로, 윈도우의 타이틀바 영역에 대한 DC를 얻기 위해서는 GetWindowDC() 함수를 사용합니다.
결과적으로, BeginPaint() 함수는 정적(Static) 출력을 하기 위해 사용합니다. 윈도우의 틀이 출력되는 것처럼 기본적으로 출력할 수 있는 것입니다. 반대로, GetDC() 함수는 동적(Dynamic) 출력을 하기 위해 사용합니다. 마우스의 동작에 따라, 혹은 키보드의 입력에 따라 동작할 때 GetDC 함수를 사용하여 바로 출력하곤합니다. 일시적인 출력이기 때문에, 다른 윈도우에 의해 가려지거나 윈도우가 변화하면 지워집니다.(이후 변화는 책임지지 않기 때문입니다.)
두 함수 모두, 사용이 끝나면 메모리를 반환하기 위해서 EndPaint(BeginPaint를 사용한 경우),ReleaseDC(GetDC를 사용한 경우)를 꼭 사용해야합니다.
이렇게 BeginPaint가 어떤 것인지 알았습니다. 이 때, 인수로 사용되는 PAINTSTRUCT 구조체에 대해서 알아보도록 하겠습니다. 해당 구조체는 HDC, rcPaint와 같은 출력에 관한 정보를 담고있는 구조체입니다. 이 구조체의 주소를 두번째 매개변수로 사용하여 정적 출력을 진행할 수 있습니다.
문자열 정렬
출력하는 문자열을 정렬해야하기 위해 다음과 같은 함수를 사용할 수 있습니다. 대부분 요소들에 대한 설정은 함수와 OR연산을 통해서 세팅할 수 있습니다. 정렬을 위한 함수의 원형은 다음과 같습니다.
UINT SetTextAlign(HDC hdc, UINT fMode);
HDC hdc는 출력하는 문자열의 HDC(Handle Device Context)이고, 두 번째 인수는 정렬하는 옵션입니다. 지원하는 옵션은 다음과 같습니다.
값 | 기능 |
TA_TOP | 지정한 좌표가 상단에 위치 |
TA_BOTTOM | 지정한 좌표가 하단에 위치 |
TA_CENTER | 지정한 좌표가 수평 중앙 좌표 |
TA_LEFT | 지정한 좌표가 수평 왼쪽 |
TA_RIGHT | 지정한 좌표가 수평 오른쪽 |
TA_UPDATECP | 지정한 좌표가 CP를 사용, 출력 후 CP 변경 |
TA_NOUPDATECP | CP를 사용하지 않고 지정한 좌표 사용, CP 변경하지 않음 |
여기서, CP란 Current Position을 의미합니다. 저희가 메모장에 글을 쓸 때, 커서가 깜빡거리듯, 그래픽을 출력할 때 다음 그래픽이 출력될 위치를 뜻합니다. 깜빡거리지 않을 뿐 똑같습니다.
TextOut3 프로젝트
실습해보기 위해서 TextOut3 프로젝트를 작성해보도록 하겠습니다.
먼저, 정렬하기 전입니다. 각 좌표에 맞게 해당 문자열이 출력됩니다. X좌표는 동일하고, Y좌표가 20씩 커지면서 아랫 줄에 출력됩니다.
TA_UPDATECP 옵션을 주어 정렬을 하니, 한 줄로 출력되는 모습을 확인할 수 있습니다.
해당 옵션은 출력 위치를 지정하는 인수를 무시하고, 항상 CP(Current Position)의 위치에 문자열을 출력하고, 출력 후 CP를 문자열의 다음 위치로 옮깁니다.
그렇기 때문에, 한 줄로 출력되는 것입니다. 이 외에도 다른 옵션들을 넣어보면 각 옵션에 맞게 문자열이 출력되는 것을 확인할 수 있습니다.
정리
이렇게 문자열을 출력하는 것에 대해서 다루었습니다. 간단한 것이지만, 가장 기초가 되는 부분입니다. 그래픽 기반의 프로그램이기 때문에 화면에 출력하는 원리와 방법이 가장 중요하다고 생각합니다. 다음 포스팅에서는 긴 문자열을 출력하고 간단한 도형을 출력해보도록 하겠습니다.
'개발 > Win32 API Programming' 카테고리의 다른 글
[Windows API] Win32 API를 활용해 키보드 입력하기 (2) | 2022.01.06 |
---|---|
[Windows API] Win32 API를 활용해 긴 텍스트, 도형, 메세지박스 출력하기 (0) | 2022.01.02 |
[Windows API] Win32 API의 기본구조, 윈도우 프로시저 (0) | 2021.12.26 |
[Windows API] Win32 API의 기본구조, WinMain-(2) (0) | 2021.11.05 |
[Windows API] 윈도우 프로젝트 생성과 WIN32 API의 기본 구조-(1) (0) | 2021.11.03 |