튜토리얼 4: Painting with Text


이번 튜토리얼에서는 윈도우의 클라이언트 영역에 "paint" 라는 텍스트를 출력하는 방법에 대해서 배워 보도록 하겠습니다. 또한 디바이스 컨텍스트(Device Context)에 대해서도 배울 것입니다. 소스코드는 여기에서 받으시면 됩니다. 


이론:

Windows 에서 텍스트(text)는 GUI 객체 타입입니다. 각 문자는 고유한 패턴에 맞춰진 수 백개의 픽셀 집합으로 이루어져 있습니다. 이것이 바로 "기록(writing)" 라는 말 대신 "그리기(painting)" 이라고 불리는 이유입니다. 일반적으로, 자신의 클라이언트 영역 안에 텍스트를 그려 넣을 수 있습니다.(사실, 자신의 클라이언트 영역 밖에다가 텍스트를 그려 넣을 수도 있지만 그것은 다른 주제입니다.) Windows 에서 화면에 텍스트를 출력하는 것은 DOS 에서 하는 것과는 많이 다릅니다. DOS 에서 스크린의 크기는 80x25 정도입니다. 하지만 Windows 에서 화면은 여러 프로그램이 함께 공유합니다. 그렇기 때문에 프로그램이 다른 프로그램 위에 기록(writing)하는 것을 막기 위해서는 규칙은 필수사항입니다. Windows 는 그리는 영역을 자신의 클라이언트 영역으로 제한함으로써 이것을 보장합니다. 윈도우(창)의 클라이언트 영역의 크기 또한 일정하지 않습니다. 사용자는 언제든지 이 크기를 변경 할 수 있습니다. 그렇기 때문에 클라이언트 영역의 크기를 동적으로 결정해야만 합니다.

클라이언트 영역에 무언가를 그리기 전에, Windows 를 통해서 반드시 권한이 있는지 확인해야만 합니다. 네, 그렇습니다. 더 이상 DOS 에서처럼 스크린의 대해서 절대권한을 갖고 있지 않습니다. Windows 을 통해서 자신의 클라이언트 영역에 대해 그리는 권한이 있는지를 반드시 확인해야만 합니다. Windows 는 여러분의 클라이언트 영역의 크기, 폰트, 색깔 그리고 그 밖의 GDI 속성들에 대해서 결정하고, 프로그램에게 디바이스 컨텍스트(Device Context) 핸들을 보내줄 것입니다. 그러면 여러분은 자신의 클라이언트 영역에 무언가를 그리는 권한으로써 디바이스 컨텍스트(Device Context) 를 이용할 수 있을 것입니다. 디바이스 컨텍스트(Device Context) 가 대체 무엇일까요? 이것은 단순히 Windows 에 의해 내부적으로 유지되는 데이터 구조체입니다. 디바이스 컨텍스트(Device Context) 는 프린터, 영상 출력처럼 각각의 장치로 구성되어 있습니다. 영상 출력을 위해서, 디바이스 컨텍스트는 보통 화면 위에 있는 각각의 윈도우로 구성되어 있습니다.

디바이스 컨텍스트의 값에는 색깔, 폰트 등과 같은 그래픽 속성들이 있습니다. 이 값은 여러분이 변경할 수 있는 기본적인 값들입니다. 이것은 GDI 관련 함수를 호출할때, 이런 속성들을 일일이 명시하는 수고를 덜어 줍니다. 쉽게 생각해서, 디바이스 컨텍스트(Device Context)는 Windows 가 여러분을 위해서 준비해 놓은 기본 환경이라고 생각하시면 됩니다. 물론 나중에 필요하다면 기본 세팅값을 변경 할 수도 있습니다.

프로그램에서 무언가를 그려야 할 때, 디바이스 컨텍스트의 핸들을 얻어야만 합니다. 일반적으로 이것을 얻는 방법에는 여러가지 방법이 있습니다. 

call BeginPaint in response to WM_PAINT message.
call GetDC in response to other messages.
call CreateDC to create your own device context


반드시 기억해야 할 것은 디바이스 컨텍스트(Device Context)의 핸들을 사용하고 나면, 반드시 그것을 하나의 메세지를 처리하는 동안에 해제시켜야 한다는 것입니다. 한 메세지를 처리하는 부분에서 얻은 핸들을 다른 메세지의 처리 부분에서 해제 시키면 안됩니다.

Windows 는 WM_PAINT 메세지를 윈도우(창)에게 보내서 자신의 클라이언트 영역을 다시 그려야 할 시간이라고 알려줍니다. Windows 는 윈도우의 클라이언트 영역에 있는 내용을 따로 저장해 놓고 있지 않습니다. 대신 클라이언트 영역을 다시 그려야 하는 상황(윈도우가 다른 창에 가려있다가 막 나올 때)이 발생하면, Windows 는 WM_PAINT 메세지를 만들어 해당 윈도우의 메세지 큐에 넣습니다. 이것은 윈도우(창)에게 자신의 클라이언트 영역을 다시 그리라는 명령을 내리는 것입니다. 여러분은 윈도우 프로시저 안에서 WM_PAINT 를 통해 어떻게 다시 그려야 하는지에 대한 정보를 반드시 얻어야 합니다. 그래서 윈도우 프로시저는 WM_PAINT 메세지가 도착해야만 클라이언트 영역을 다시 그릴 수 있는 것 입니다.

또 다른 개념으로 여러분은 다시 그려야 하는 부분(Invalid Rectangle)에 대해 타협을 봐야만 합니다. Windows 는 클라이언트 영역 안에서 다시 그려져야 하는 부분을 가장 작은 크기의 사격형으로 나타낸 것을 유효하지 않은 사각형(Invalid Rectangle)이라고 정의합니다. Windows 는 유효하지 않은 사각형을, 다시 그려져야 하는 클라이언트의 영역 안에서 가장 작은 사각형 형태로 정의합니다. Windows 는 유효하지 않은 사각형을 윈도우 클라이언트 영역에서 찾으면, 그 윈도우에게 WM_PAINT 메세지를 전달합니다.

WM_PAINT 메세지를 받으면, 그 윈도우는 그냥 두어도 되는 부분과 다시 그려야 하는 유요하지 않은 영역의 좌표를 얻을 수 있습니다. 반드시 WM_PAINT 메세지에 대한 처리로 BeginPaint 을 호출해서 유효하지 않은 사각형을 다시 그려야 합니다. 만약 WM_PAINT 메세지를 처리하지 않았다면, 적어도 유효하지 않은 영역을 유효하게 하기 위해서 DefWindowProc 나 ValidateRect 를 호출해야 합니다. 그렇지 않으면 Windows 는 계속해서 WM_PAINT 메세지를 보내올 것입니다.
아래는 WM_PAINT 메세지를 처리할 때, 수행해야 하는 절차입니다.

Get a handle to device context with BeginPaint.
Paint the client area.
Release the handle to device context with EndPaint


유효하지 않은 영역을 정확하게 확인해야 할 필요는 없습니다. BeginPrint 를 호출하면 알아서 처리해 줍니다. BeginPaint 와 EndPaint 사이에 클라이언트 영역을 그리기 위한 그 어떤 GDI 함수도 사용할 수 있습니다. 함수 대부분 파라미터로 디바이스 컨텍스트(Device Context) 핸들을 필요로 합니다.

내용:

이제 클라이언트 영역 중앙에 "Win32 assembly is great and easy!" 라는 문자열을 출력하는 프로그램을 만들어 볼 것입니다. 
 

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName  db "Our First Window",0
OurText  db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
        .WHILE TRUE
                invoke GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
        .ENDW
        mov     eax,msg.wParam
        ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL hdc:HDC
    LOCAL ps:PAINTSTRUCT
    LOCAL rect:RECT
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .ELSEIF uMsg==WM_PAINT
        invoke BeginPaint,hWnd, ADDR ps
        mov    hdc,eax
        invoke GetClientRect,hWnd, ADDR rect
        invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
                DT_SINGLELINE or DT_CENTER or DT_VCENTER
        invoke EndPaint,hWnd, ADDR ps
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .ENDIF
    xor   eax, eax
    ret
WndProc endp
end start


 

분석:

대부분의 코드는 튜토리얼 3의 예제와 같습니다. 다른 부분 중 중요한 것에 대해서만 설명하도록 하겠습니다. 

LOCAL hdc:HDC 
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT


WM_PAINT 섹션에서 GDI 함수에 의해 사용되는 지역 변수들 입니다. hdc 는 BeginPaint 함수 호출 후 반환되는 디바이스 컨텍스트(Device Context)의 핸들을 저장하기 위해 사용됩니다. ps 는 PAINTSTRUCT 구조체 입니다. 일반적으로 PS 에 있는 값을 사용하지 않습니다. 이 값은 BeginPaint 함수로 전달 되고 Windows 가 특정한 값으로 채워 넣을 것입니다. 여러분은 클라이언트 영역을 그리는 것이 끝나면 EndPaint 함수에게 이 값을 전달하면 됩니다. rect 는 아래에서 설명할 RECT 구조체 입니다.
 

RECT Struct
    left           LONG ?
    top           LONG ?
    right        LONG ?
    bottom    LONG ?
RECT ends


Left 와 top 은 사각형 좌측상단 모서리의 좌표입니다. 그리고 Right와 bottom 은 우측하단 모서리 좌표입니다. 한가지 기억할 것은 영점이 되는 x-y 좌표는 클라이언트 영역의 좌측상단이라는 것입니다. 그래서 점(point) y=10 은 점(point) y=0 의 아래쪽에 위치합니다. 

invoke BeginPaint,hWnd, ADDR ps
mov    hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps


WM_PAINT 메세지 처리 부분에서, 그리고(point) 싶은 윈도우의 핸들과 초기화 되지 않은 PAINTSTRUCT 구조체를 파라미터로 BeginPaint 를 호출했습니다. 성공적으로 호출이 끝나면, eax 에는 디바이스 컨텍스트 핸들이 들어 있습니다. 그 후에 클라이언트 영역의 크기를 얻기 위해서 GetClientRect 를 호출하였습니다. 영역의 크기는 DrawText 에서 전달하는 파라미터 중 하나인 rect 변수에 채워집니다. DrawText 의 문법은 다음과 같습니다.

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD


DrawText 는 고급의 텍스트 출력 API 함수입니다. 이 함수는 단어 감싸기(word wrap), 가운데 정렬 등과 같이 직접 처리하기엔 까다로운 것들을 처리해 줍니다. 덕분에 우린 문자열을 찍는 것에만 신경 쓸 수 있습니다. 이것보다 더 하위레벨(low-level)로는 다음 튜토리얼에서 살펴볼 TextOut 이 있습니다. DrawText 은 사각형 여역 안에 문자열을 맞출 수 있습니다. 그리고 텍스트를 그릴때 현재의 디바이스 컨텍스트에서 선택되어 있는 폰트, 글씨 색깔, 배경 색을 사용합니다. 각 라인은 사각형의 영역 안에 감싸 집니다. 이 함수는 디바이스 유닛(device units) 단위(예제에서는 픽셀)로 출력 텍스트의 높이를 반환(return)합니다. 파라미터를 살펴 봅시다.

hdc  디바이스 컨텍스트(device context)의 핸들
lpString  사각형 영역 안에 그릴 문자열의 포인터. 반드시 널로 끝나는 문자열(null-terminated)이어야 합니다. 그렇지 않을 경우에는 다음 파라미터인 nCount 에 문자열의 길이를 명시해 주어야 합니다.
nCount  출력할 문자의 갯수. 만약 문자열이 null 로 끝난다면, nCount 의 값으로 -1 을 넣어야만 합니다. 그렇지 않다면, 문자열에서 출력하기를 원하는 문자 갯수를 입력해야만 합니다.
lpRect  문자열이 들어갈 사각형(RECT 타입 구조체)의 포인터. 이 사각형은 클리핑(clipping)된 사각형입니다. 즉, 문자열을 이 사각형 밖에 그릴 수는 없습니다.
uFormat 이 값은 사각형 안에 문자열을 어떻게 출력하는 방법을 나타냅니다. 이 값들은 "or" 연산자를 통해 조합(combine) 할 수 있습니다.

DT_SINGLELINE  한 줄짜리 텍스트를 의미합니다.
DT_CENTER  가로 중앙에 텍스트가 위치합니다.
DT_VCENTER  세로 중앙에 텍스트가 위치합니다. DT_SINGLELINE 과 함께 사용되어야만 합니다.


클라이언트 영역에 그리는 것은 마친 후, 디바이스 컨텍스트 핸들을 해제하기 위해서 반드시 EndPaint 함수를 호출해야 합니다. 주의해야 할 것들을 요약하면 다음과 같습니다.

  • WM_PAINT 메세지에 대한 처리를 한 후, BeginPaint 후에 EndPaint 를 꼭 호출해야 합니다.
  • 클라이언트 영역에 무언가를 하려면, BeginPaint 와 EndPaint 함수 호출 코드 사이에서 해야 합니다. 
  • 다른 메세지를 처리한 후에, 클라이언트 영역을 다시 그려주고 싶다면 두 가지 방법이 있습니다.
    - 먼저 GetDC 를 호출하고 그리고 싶은 것을 그린 뒤, 끝으로 ReleaseDC 를 호출 하십시오.
    - InvalidateRect 또는 UpdateWindow 함수를 호출해서 클라이언트 영역 전체를 무효하게 만들면 Windows 는 WM_PAINT 메세지를 발생시킬 것입니다. 그리고 여러분은 WM_PAINT 섹션에서 그리면 됩니다.