'어셈블리 강좌'에 해당되는 글 2건

  1. 2010.04.14 [MASM 강좌] 튜토리얼 4 : Painting with Text
  2. 2010.04.11 [MASM 강좌] 튜토리얼 3 : A Simple window (1)


 

튜토리얼 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 섹션에서 그리면 됩니다.

 

신고

튜토리얼 3: A Simple Window


이번 튜토리얼에서는 화면 위에 모든 기능을 가지고 있는 윈도우를 생성하는 Windows 프로그램을 만들어 보도록 하겠습니다. 예제 파일은 여기에서 받으시기 바랍니다.


이론:

Windows 프로그램은 GUI 를 위해서는 API 함수에 매우 의존적인데, 이는 사용자나 개발자 모두에게 도움이 됩니다. 사용자는 Windows 프로그램들의 GUI가 다 비슷하기 때문에, 처음 접하는 프로그램이라도 별도의 GUI 작동법을 배우지 않아도 됩니다. 그리고 프로그래머에게는 충분히 검증되어 바로 사용할 수 있는 GUI 코드를 제공합니다. 한편으로는 프로그래머에게  조금 더 복잡해 졌습니다. 창, 메뉴, 아이콘들 같은 GUI 오브젝트(object)들을 만들거나 조작하기 위해서 프로그래머는 엄격한 규칙을 지켜야만 합니다. 하지만 이러한 것들은 모듈화 프로그래밍이나 OOP 패러다임으로 극복 할 수 있을 것 입니다.

다음은 화면 위에 윈도우를 띄우기 위해서 요구되는 절차의 아웃라인(outline)입니다.

1. 프로그램의 인스턴스 핸들을 가져옴(필수사항) 
2. 커맨드 라인을 가져옴(프로그램에서 커맨드 라인이 필요하지 않다면 필수사항 아님) 
3. 윈도우 클래스(window class) 등록함(MessageBox 나 dialogbox 처럼 미리 정의되어 있는 윈도우 타입을 사용할 것이 아니라면 필수사항) 
4. 윈도우 만듦(필수사항) 
5. 바탕화면 위에 창 보이기(창이 즉시 보여지는지길 원한다면 필수사항) 
6. 창의 클라이언트 영역을 새로고침 
7. Windows의 메세지를 체크하기 위한 무한루프로 들어감 
8. 메세지가 도착하면, 그 창을 책임지고 있는 특정 함수가 처리함 
9. 사용자가 창을 닫으면 프로그램은 종료됨

위에서 알 수 있듯이, Windows 프로그램의 구조는 DOS 프로그램에 비해서 다소 복잡합니다. 하지만 Windows 의 세계는 DOS 의 세계와 명백히 다릅니다. Windows 프로그램은 서로 간에 평화롭게 공존 할 수 있어야만 합니다. 그래서 다소 엄격한 규칙을 지켜야만 하는 것입니다. 뿐만 아니라 여러분은 프로그래머로서 자신의 프로그래밍 스타일이나 습관에 대해서 더 엄격해져야만 합니다.



내용:

아래에 간단한 윈도우(창) 프로그램을 위한 소스 코드가 있습니다. 이 끔찍한 Win32 어셈블리 프로그래밍의 세부내용을 살펴보기 전에, 여러분의 마음을 편안하게 해 줄 요점을 몇 가지 짚어 드리겠습니다.

  • Windows의 상수, 구조체, 함수 프로토타입들이 있는 포함 파일(include file)을 사용해야 하고, .asm 파일의 시작 부분에 포함(include)을 시킵니다. 이렇게 하는것이 많은 수고와 타이핑을 줄일 수 있습니다. 현재로서는 가장 완벽한 MASM include 파일은 hutch 의 windows.inc 이며, 그 또는 저의 홈페이지에서 다운로드 받으실 수 있습니다. 또한 여러분만의 상수나 구조체를 정의 하실 수 있습니다. 하지만 이 경우 별도의 include 파일로 분리하여 저장 하시는 것이 좋습니다.
  • 여러분의 프로그램 내부적으로 사용하는 외부 라이브러리(import library)는 includelib 지시자를 사용하여 명시하시기 바랍니다. 예를 들어 프로그램에서 MessageBox 를 호출한다면, 다음 행을 .asm 파일의 시작부분에 추가시키십시오.
     includelib user32.lib
    이 지시자는 MASM으로 하여금 여러분의 프로그램이 외부 라이브러리에 있는 함수를 사용할 수 있도록 해 줍니다. 만약 프로그램에서 여러 개의 라이브러리를 사용한다면, 각각의 라이브러리에 대해서 includelib 를 추가시키면 됩니다. IncludeLib 지시자를 사용하면, 링크를 할 때 외부 라이브러리들을 신경쓰지 않아도 됩니다. /LIBPATH 를 사용하면 라이브러리들이 있는 경로를 변경 할 수 있습니다.
  • 여러분의 include 파일안에서 API 함수의 프로토타입, 구조체, 상수들을 선언할 때, Windows include 파일들에서 사용되고 있는 이름을 대소문자까지 똑같이 사용하시기 바랍니다. 이렇게 해야 Win32 API 도움말을 볼 때, 헷갈리지 않습니다.
  • 자동으로 어셈블링 하는 메이크파일(makefile) 을 사용하십시오. 타이핑하는 수고를 덜 수 있습니다.

.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib            ; user32.lib 과 kernel32.lib 안에 있는 함수를 사용하기 위해서
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD


.DATA   ; 초기화한 데이터 
ClassName db "SimpleWinClass",0   ; window class 이름
AppName db "Our First Window",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   ; main 함수 호출
invoke ExitProcess, eax   ; 프로그램 종료. exit code 는 WinMain 가  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   ; wc 의 멤버변수 값들을 채움
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInstance
    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                       ; window class 를 등록
    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,CmdShow   ; 화면에 window 를 출력
    invoke UpdateWindow, hwnd   ; client 영역을 새로그림


    .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   ; 종료코드를 eax 에 채워서 리턴
    ret
WinMain endp


WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY   ; 사용자가 창을 닫을 경우
        invoke PostQuitMessage,NULL   ; 프로그램을 종료
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam   ; 기본 메세지들 처리
        ret
    .ENDIF
    xor eax,eax
    ret
WndProc endp

end start


 

분석:

아마도 간단한 Windows 프로그램치고는 코드가 너무나 많다고 생각하셨을 것입니다. 하지만 이 코드들은 단지 이 코드에서 저 코드로 복사해서 써도 되는 *템플릿* 코드일 뿐 입니다. 아니면 원한다면, 이 코드의 일부를 라이브러리로 만들어  시작(prologue)과 끝(epilogue) 코드로 사용하고 가운데만 채워서 사용해도 됩니다. 위 코드는 WinMain 라는 함수 안에서 사용 해야 하지만, 사실 이것은 C 컴파일러의 규칙입니다. C 컴파일러는 자잘한 것들에 신경쓰지 않고, WinMain 코드를 작성할 수 있도록 해 줍니다. C 컴파일러는 작성된 코드를 시작(prologue)과 끝(epilogue)을 이용하여 합치기 때문에 함수명으로 반드시 WinMain 를 사용해야 합니다. 어셈블리어에는 이런 제약이 없습니다. 함수명을 WinMain 대신 다른 이름을 사용해도 되고, 심지어 함수가 없어도 됩니다.

자, 그럼 부디 각오하세요. 이 튜토리얼은 매우 매우 깁니다. 죽을 각오로 이 프로그램을 분석해 봅시다!

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

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


처음 세 줄은 반드시 있어야 합니다. .386 은 MASM 에게 이 프로그램이 80386 명령어 세트를 사용한다고 알립니다. .model fat, stdcall 은 MASM 에게 이 프로그램이 평면 메모리 주소 모델(flat memory adressing model)을 사용한다고 알립니다. 그리고 이 프로그램이 기본값으로 stdcall 파라미터 전송 방식을 사용한다고 알립니다.
다음 WinMain 을 위한 함수 프로토타입입니다. 나중에 WinMain 을 호출할 것이기 때문에, invoke 를 할 수 있도록 반드시 그 전에 함수의 프로토타입을 정의해 두어야 합니다.

소스코드 시작 부분에서 반드시 windows.inc 를 include 해야 합니다. 이것은 프로그램에서 사용되는 중요한 구조체와 상수를 담고 있습니다. windows.inc 파일은 단순한 텍스트 파일입니다. 어떤 문서 편집기도 열어 볼 수 있습니다. (아직까진) windows.inc 가 모든 구조체와 상수를 포함하고 있지 않다는 것을 알아 주시기 바랍니다. hutch 와 제가 아직 작성중입니다. 파일에 빠진 항목이 있다면 추가 시키셔도 됩니다.

이 프로그램은 user32.dll 과 kernel32.dll 에 들어있는 API 함수(user32.dll: CreateWindowEx, RegisterWindowClassEx, kernel32.dll: ExitProcess)를 사용할 것이기에, 반드시 이 두개의 임포트 라이브러리와 링크를 시켜야 합니다. 질문: 어떤 임포트 라이브러리를 링크해야 하는가? 답변: 프로그램에서 사용하는 API 함수가 어디에 있는지 알아야 합니다. 예를 들어, gdi32.dll 안에 있는 API 함수를 호출한다면 gdi32.lib 를 링크해야 합니다.
 
이것은 MASM 방식입니다. TASM 의 임포트 라이브러리 링크 방식은 훨씬 간단합니다. 그냥 import32.lib 파일 하나만 링크하면 됩니다.

.DATA
    ClassName db "SimpleWinClass",0
    AppName  db "Our First Window",0
.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

 
다음으로 "DATA" 섹션입니다.
.DATA 안에 두개의 0으로 끝나는 문자열(ASCIIZ 문자열)을 선언했습니다. 윈도우 클래스(window class)의 이름 ClassName 과 윈도우의 이름인 AppName 입니다. 두 개의 변수는 초기화 시켰습니다.
.DATA? 안에도 두개의 변수가 선언되어 있습니다. hInstance(프로그램의 인스턴스 핸들) 과 CommandLine(프로그램의 커맨드 라인) 입니다. 익숙치 않은 HINSTANCE 와 LPSTR 은 DWORD 의 다른 이름일 뿐 입니다. 이것들은 windows.inc 안에서 확인 할 수 있습니다. .DATA? 섹션 안에 있는 변수들은 초기화되지 않았다는 것을 기억하십시오. 그 이유는 그것들은 프로그램이 시작될 때, 어떤 특별한 값을 가지고 있을 필요가 없기 때문입니다. 하지만 나중에 사용할 것이기 때문에 공간을 할당해 두어야 합니다.
 

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

.CODE 에는 여러분의 모든 명령들이 있습니다. 코드들은 반드시 <starting label>: 와 <starting label> 사이에 있어야 합니다. label의 이름은 중요하지 않습니다. 중복되지 않으면서 MASM 네이밍 규칙에 어긋나지 않는 것이라면 어떤 이름이든 사용 할 수 있습니다.
맨 처음 나오는 명령어는 프로그램의 인스턴스 핸들을 얻어오는 GetModuleHandle 함수 호출입니다. Win32 에서 인스턴스 핸들과 모듈 핸들은 똑같은 것입니다. 인스턴스 핸들은 프로그램의 ID 라고 생각하면 됩니다. 이것은 프로그램에서 반드시 호출하게 되는 몇몇 API 함수에서 파라미터로 사용하기 때문에, 시작할 때 미리 얻어오는 것이 좋습니다.
요약: 실제로 win32 에서 인스턴스 핸들은 메모리에 올라간 프로그램을 가리키는 주소

Win32 함수에서 리턴되는 즉시, 함수의 리턴값은 eax 에 저장 됩니다. 그리고 다른 값들은 함수를 호출할 때 파라미터로 전달한 변수들을 이용해 리턴합니다.
여러분이 호출하는 Win32 함수는 거의 대부분 세그먼트 레지스터, ebx, edi, esi, ebp 레지스터는 변경시키지 않을 것입니다. 반대로 ecx, edx 는 Win32 함수에서 리턴되어 돌아올 때, 그 값이 변경되어 질 수 있어 항상 값을 확신할 수는 없습니다.
요약: API 함수 호출 이후에도 eax, ecx, edx 의 값이 그대로 보존될 거라는 기대는 하지 말 것

GetCommandLine 함수 호출 다음에 오는 코드는 이렇습니다. API 함수를 호출할 때, 리턴값은 eax 에 저장되어 있다고 생각하면 됩니다. 여러분이 만든 함수가 Windows 에 의해서 호출 된다면, 다음 규칙을 지켜야만 합니다. 함수에서 빠져나오기 전에 세그먼트 레지스터, ebx, edi, esi, ebp 값을 반드시 보존하고 본래대로 복구시켜 놓아야 합니다. 그렇지 않으면, 프로그램은 얼마 지나지 않아서 충돌을 일으킬 것입니다. 이것은 여러분의 윈도우 프로시저나 windows 콜백함수 모두 마찬가지입니다.

프로그램에서 command line 을 사용하지 않을 것이라면, GetCommandLine 호출은 할 필요가 없습니다. 이 예제에서는 프로그램에서 필요할 경우 어떻게 해야 하는지를 보여주기 위해서 호출하였습니다.
 
다음은 WinMain 함수 호출입니다. 이 함수는 네 개의 파라미터를 받습니다. 우리 프로그램의 인스턴스 핸들, 우리 프로그램의  인스턴스 이전 인스턴스 핸들(previous instance handle), 커맨드라인, 처음 보여질 윈도우 상태. Win32 에서는 이전 인스턴스(previous instance)가 없습니다. 모든 프로그램은 독립적인 메모리 공간을 가지고 있기 때문에, hPrevInst 의 값은 항상 0 입니다. hPrevInst 는 모든 프로그램의 인스턴스들이 한 메모리 공간에 공존하고, 어느 인스턴스가 첫번째 인스턴스인지 알아야 했던 Win16 시절의 잔재입니다. Win16 에서는 hPrevInst 의 값이 NULL 인 인스턴스가 첫번째 인스턴스였습니다.
 
요약: 함수 이름을 반드시 WinMain 으로 할 필요는 없습니다. 사실 이름에 대해서는 전혀 제약이 없습니다.
WinMain 과 같은 함수를 사용할 필요는 전혀 없습니다.  WinMain 함수 안에 있는 코드를 그대로 복사해서 GetCommandLine 다음에 붙여넣어도 프로그램은 전혀 이상없이 실행됩니다.
WinMain 함수가 끝나서 리턴될 때, 종료코드(exit code)는 eax 에 저장됩니다. 프로그램을 종료시키는 ExitProcess 함수를 호출할 때, 파라미터로 이 종료코드(exit code) 를 사용합니다.

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

이 라인은 WinMain 를 함수 선언하는 코드입니다. PROC 키워드 다음에 파라미터:타입 형태의 쌍들이 나옵니다. 이것들은 WinMain 을 호출할 때, 전달해야 하는 파라미터들입니다. 이 값들은 스택을 이용하지 않고, 이름을 통해서도 참조할 수도 있습니다. 게다가 MASM 은 함수를 위한 상단과 하단을 생성할 것입니다. 덕분에 우리는 함수를 들어가고 빠져 나올 때, 스택 프레임을 신경쓰지 않아도 됩니다.

LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

LOCAL 지시자는 함수 안에서 사용되는 지역변수의 메모리를 스택에 할당합니다. LOCAL 지시자들은 반드시 PROC 지시자 바로 다음에 나와야 합니다. LOCAL 지시자 바로 다음에는 <지역변수 이름>:<변수 타입> 이 나옵니다. 즉, LOCAL wc:WNDCLASSEX 는 MASM 이 wc 라는 변수 이름으로 WNDCLASSEX 구조체의 사이즈만큼 스택에 메모리를 할당하게 합니다. 코드 안에서 어렵게 스택을 이용하지 않고, wc 를 통해 메모리에 접근할 수 있습니다. 저는 이것은 정말 신의 축복이라고 생각합니다. 반면 지역변수는 변수를 선언한 함수 밖에서는 사용할 수 없으며, 함수를 빠져나와 호출한 곳으로 되돌아갈 때, 자동으로 해제(destory)됩니다. 또 하나의 약점은 지역변수는 함수에 들어갈 때, 자동으로 스택 메모리에 할당되기 때문에 자동으로 초기화 할 수 없습니다.(역주: 지역변수는 초기에 쓰레기 값이 들어 있다는 이야기. 초기값이 0 이 아님) 그래서 LOCAL 지시자 다음에 원하는 값을 명시적으로 지정해 주어야 합니다.

    

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  hInstance
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

 

위 코드들의 개념은 매우 간단합니다. 단지 몇 줄의 명령어 코드로 완성됩니다. 이 명령어 행들에 숨어있는 개념은 windows class 입니다. window class 는 단지 윈도우의 설계구조나 스팩을 보여주는 것 뿐입니다. 아이콘이나 커서, 콜백 함수, 색깔 등과 같은 윈도우의 중요한 특성들을 정의합니다. window class 를 통해서 윈도우를 만듭니다. 이것은 객체 지향의 한 컨셉입니다. 만약 똑같은 특성의 윈도우를 하나 이상 만들려고 한다면, 한 공간에 이 특성들을 모두 저장하고 필요할 때마다 이것을 참조하도록 하면 됩니다. 이 방법은 중복된 정보가 저장되는 것을 막아 많은 메모리 공간을 절약하도록 해 줍니다. 생각해 보세요. 과거에 Windows 가 나왔을 당시에는 메모리 칩셋이 어마어마하게 비쌌으며, 대부분의 컴퓨터의 메모리는 1MB 밖에 되지 않았습니다. Windows 는 엄청나게 효율적으로 이 부족한 메모리를 사용해야만 했습니다.
요컨데 윈도우를 정의할 때는 창을 만들기에 앞서서, 원하는 윈도우 특성들을 WNDCLASS 나 wndclassex 구조체에 채워서 RegisterClass 나 RegisterClassEx 를 호출해야만 합니다. 만드려는 각각의 윈도우 타입을 위해서 window class 를 한번만 등록해 주면 됩니다. Windows 는 버튼이나 데이터 박스처럼 미리 정의해 놓은 몇 개의 window class 을 가지고 있습니다. 이런 윈도우들(또는 컨트롤)을 사용하기 위해서는 windows class 를 따로 등록할 필요 없이, 미리 정의된 class name 으로 CreateWindowEx 를 호출하기만 하면 됩니다.
WNDCLASSEX 에서 가장 중요한 핵심 멤버는 lpfnWndProc 입니다. lpfn 는 함수를 가리키는 긴 포인터(Long Pointer to Function)입니다. 사실 Win32 에서 "가까운(near)" 또는 "먼(far)" 포인터라는 것은 없습니다. 새로운 FLAT 메모리 모델에서는 단지 포인터 하나 밖에 없습니다. 이것은 Win16 시절의 잔재물입니다.
각각의 윈도우 클래스(window class)는 반드시 윈도우 프로시저라는 함수를 가지고 있어야만 합니다. 이 윈도우 프로시저는 window class 에 의해서 생겨난 윈도우의 모든 메세지 처리를 담당합니다. Windows 는 사용자의 키보드 또는 마우스 입력과 같이 중요한 이벤트의 발생을 알리는 메세지를 윈도우 프로시저에게 전달합니다. 이것은 지능적으로 각각의 윈도우가 받은  메세지를 응답해 주기 위해서 윈도우 프로시저에게 전달합니다. 여러분은 대부분 윈도우 프로시저에 이벤트 핸들러를 작성하는데 많은 시간을 소요할 것입니다.
아래는 WNDCLASSEX 의 멤버에 대한 설명입니다.

WNDCLASSEX STRUCT DWORD
  cbSize              DWORD      ?
  style                DWORD      ?
  lpfnWndProc      DWORD      ?
  cbClsExtra        DWORD      ?
  cbWndExtra       DWORD      ?
  hInstance          DWORD      ?
  hIcon               DWORD      ?
  hCursor            DWORD      ?
  hbrBackground  DWORD      ?
  lpszMenuName  DWORD      ?
  lpszClassName  DWORD      ?
  hIconSm           DWORD      ?
WNDCLASSEX ENDS


cbSize:
WNDCLASSEX 구조체의 byte 크기입니다. SIZEOF 명령을 통해 값을 구할 수 있습니다.
style: 이 클래스가 만들 윈도우 스타일입니다. "or" 연산을 통해 여러가지 스타일을 섞을(combine) 수 있습니다.
lpfnWndProc: 이 클래스가 만들 윈도우를 책임지는  윈도우 프로시저 주소입니다.
cbClsExtra: window-class 구조체에 이어서 할당되는 여분(extra)의 바이트 크기를 명시합니다. 운영체제에서는 이 공간을 0 으로 초기화 합니다. window class 의 추가의 특별한 데이터를 저장하는데 사용합니다.
cbWndExtra: 윈도우 인스턴스에 이어서 할당되는 여분(extra)의 바이트 크기를 명시합니다. 운영체제는 이 공간을 0 으로 초기화 합니다. 만일 리소스 파일 안에서 CLASS 지시자를 이용해 만든 다이어로그 박스를 등록하려고 WINDCLASS 구조체를 사용한다면, 이 공간을 DLGWINDOWEXTRA 로 세팅해야 합니다.
hInstance: 모듈의 인스턴스 핸들입니다.
hIcon: 아이콘의 핸들입니다. LoadIcon 함수를 호출하면, 값을 구할 수 있습니다.
hCursor: 커서의 핸들입니다. LoadCursor 함수를 호출하면, 값을 구할 수 있습니다.
hbrBackground: 이 클래스가 만들 윈도우의 배경색입니다.
lpszMenuName: 이 클래스가 만들 윈도우의 기본 메뉴 핸들입니다.
lpszClassName: 윈도우 클래스의 이름입니다.
hIconSm: window class 와 연관된 작은 아이콘(small icon)의 핸들(handle)값입니다. 만약 값이 NULL 이라면 시스템은 작은 아이콘으로 사용할 적정 크기의 아이콘을 위해, hIcon 멤버가 정의한 아이콘 리소스를 찾습니다.


invoke CreateWindowEx, NULL,\
  ADDR ClassName,\
  ADDR AppName,\
  WS_OVERLAPPEDWINDOW,\ 
  CW_USEDEFAULT,\ 
  CW_USEDEFAULT,\ 
  CW_USEDEFAULT,\ 
  CW_USEDEFAULT,\ 
  NULL,\ 
  NULL,\ 
  hInst,\ 
  NULL

 윈도우 클래스를 등록 한 후, 그 윈도우 클래스를 바탕으로 윈도우를 생성하기 위해서 CreateWindowEX 를 호출합니다. 이 함수의 파라미터는 12 개 입니다.

CreateWindowExA proto dwExStyle:DWORD,\
   lpClassName:DWORD,\
   lpWindowName:DWORD,\
   dwStyle:DWORD,\
   X:DWORD,\
   Y:DWORD,\
   nWidth:DWORD,\
   nHeight:DWORD,\
   hWndParent:DWORD ,\
   hMenu:DWORD,\
   hInstance:DWORD,\
   lpParam:DWORD


각각의 파라미터에 대해서 자세히 살펴봅시다.

dwExStyle : 추가적인 윈도우 스타일입니다. 옛날 CreateWindow 에서 추가된 새로운 파라미터입니다. Windows 95 와 NT 를 위한 새로운 윈도우 스타일을 지정할 수 있습니다. dwStyle 에 일반적인 윈도우 스타일을 지정 할 수 있습니다. 하지만 최상위 윈도우처럼 특별한 스타일을 원한다면, 반드시 이곳에 명시해야만 합니다. 추가적인 윈도우 스타일을 사용하지 않다면 NULL 로 채우면 됩니다.
lpClassName : (요구사항). 이 윈도우의 템플릿으로 사용할 윈도우 클래스(window class) 이름의 ASCIIZ 문자열 주소입니다. 이 클래스는 여러분만의 클래스가 될 수도 있고, 윈도우에서 미리 선언한 클래스가 될 수도 있습니다. 위에서 언급한 것 처럼, 여러분이 만드는 모든 윈도우는 윈도우 클래스를 가지고 만들어 져야 합니다.
lpWindowName : 윈도우 이름의 ASCIIZ 문자열 주소입니다. 이 값은 윈도우 제목표시줄에 나타날 것입니다. 만약 값이 NULL 이라면, 창의 제목 표시줄에는 아무것도 나타나지 않습니다.
dwStyle :  윈도우의 스타일. 윈도우의 모양새를 이곳에서 정의할 수 있습니다. 이곳에 NULL 값을 넣을 경우, 윈도우에는 최대-최소 버튼이나 닫기 버튼과 같은 시스템 메뉴 박스가 보이지 않습니다. 이런 윈도우는 많이 사용되지 않을 것입니다. 이 경우 창을 닫기 위해서는 Alt+F4 를 눌러야 합니다. 가장 일반적인 윈도우 스타일은 WS_OVERLAPPEDWINDOW 입니다. 윈도우 스타일은 단순한 비트 플래그(bit flag)입니다. 그러므로 원하는 윈도우 모양을 만들기 위해서 몇몇 윈도우 스타일을 "or" 연산을 사용해서 혼합할 수 있습니다.  WS_OVERLAPPEDWINDOW 스타일은 이 방법을 통해 만들어진 가장 일반적인 윈도우 스타일 입니다.
X,Y : 윈도우의 왼쪽 상단의 좌표입니다. 일반적으로 이 값으로 CW_USERDEFAULT 를 사용해서, windows 가 알아서 창을 놓을 위치를 선택하도록 합니다.
nWidth, nHeight : 픽셀단위의 윈도우 폭과 높이입니다. 역시 CW_USERDEFAULT 를 사용하여 Windows 가 폭과 높이를 결정하도록 할 수 있습니다.
hWndParent : 이 창의 부모 창(존재할 경우) 핸들입니다. 이 파라미터는 Windows 에게 이 창이 다른 창의 자식인지 아닌지, 그리고 자식 창라면 어느 창이 부모인지 알려 줍니다. 여기서 부모-자식 관계가 다중문서 인터페이스(MDI)에서 말하는 관계가 아니라는 것을 명심하세요. 자식 창은 부모창의 클라이언트 영역에 바인딩되지 않습니다. 이 관계는 구체적으로 윈도우 내부에서 사용하기 위한 것입니다. 만약 부모창이 파괴될 때, 모든 자식 창들도 자동으로 함께 파괴됩니다. 이것은 매우 간단합니다. 예제에서는 단 하나의 윈도우 밖에 없기 때문에 이 파라미터의 값을 NULL 로 하였습니다.
hMenu : 윈도우 메뉴의 핸들입니다. 클래스 메뉴를 사용한다면 값은 NULL 이 됩니다. WNDCLASSEX 구조체의 lpszMenuName 멤버를 다시한번 보세요. lpszMenuName 는 윈도우 클래스의 기본(default) 메뉴를 명시합니다. 이 윈도우 클래스를 통해 만들어진 모든 윈도우는 기본적으로 같은 메뉴를 갖습니다. 만약 특별한 윈도우를 만들기 위해서 별도의(overriding) 메뉴를 사용한다면 이 hMenu 파라미터를 사용하면 됩니다. hMenu 는 사실 두 가지 용도로 사용되는 파라미터입니다. 만약 여러분이 만들려고 하는 윈도우가 미리 정의된 윈도우 타입(예를들어 컨트롤)이라면, 이 컨트롤은 메뉴를 가질 수 없습니다. 대신 hMenu를 컨트롤의 ID 로 사용합니다. Windows 는 hMenu 가 진짜 메뉴의 핸들인지 아니면 컨트롤의 ID인지를 lpClassName 파라미터를 통해 알 수 있습니다. 만약 이 값이 미리 정의된 윈도우 클래스 이름이라면, hMenu 는 컨트롤 ID 인 것입니다. 반대로 그렇지 않다면 윈도우 메뉴의 핸들로 인식하는 것입니다.
hInstance: 윈도우(창)를 생성하는 프로그램의 모듈을 위한 인스턴스 핸들입니다.
lpParam: 윈도우(창)에 전달할 데이터의 구조체를 가리키는 추가적인(optional) 포인터 입니다. 윈도우가 CLIENTCREATESTRUCT 데이터를 전달하기 위해서 MDI 를 사용합니다. 일반적으로 CreateWindow() 를 통해 전달할 데이터가 없을 경우 NULL 값을 사용합니다. 윈도우는 이 파라미터의 값을 GetWindowLong 함수를 호출하여 가져올 수 있습니다. 

mov   hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

CreateWindowEx 가 성공적으로 리턴했을 때, 윈도우 핸들을 eax 를 통해 전달합니다. 나중에 이 값을 사용하기 위해서는 보관하고 있어야 합니다. 우리가 방금 만든 윈도우가 자동으로 화면에 출력되진 않습니다. 윈도우 핸들과 화면에 출력될 때의 *출력 상태*를 가지고 ShowWindow 를 호출해야 합니다. 그 다음에서야 UpdateWindow 를 호출해서 클라이언트 영역이 다시 그려지도록 할 수 있습니다. 이 함수는 클라이언트 영역의 컨텐츠를 갱신(update)할 때 매우 유용합니다. 그렇지만 이 함수 호출은 생략할 수 있습니다.  


.WHILE TRUE
   invoke GetMessage, ADDR msg,NULL,0,0
   .BREAK .IF (!eax)
   invoke TranslateMessage, ADDR msg
   invoke DispatchMessage, ADDR msg 
.ENDW

이젠 윈도우가 화면에 나타납니다. 하지만 외부의 입력을 받을 수는 없습니다. 그래서 적절한 입력 이벤트에 대한 정보를 주어야 합니다. 이것을 메세지 루프를 통해서 해결합니다. 각 모듈에는 단 하나의 메세지 루프가 있습니다. 이 메세지 루프는 GetMessage 함수를 호출해서 Windows 로부터 메세지를 계속 체크합니다. GetMessage 는 윈도우에게 MSG 구조체를 가리키는 포인터를 전달합니다. 이 MSG 구조체는 Windows 가 모듈의 윈도우(창)에게 전달하고 싶은 메세지에 대한 정보가 있습니다. GetMessage 함수는 모듈의 윈도우(창) 에 대한 메세지가 있기 전까지는 리턴하지 않습니다. 이럴때 Windows 는 제어권을 다른 프로그램에게 줄 수 있습니다. 이것이 Win16 플랫폼의 멀티태스킹 기법의 모습입니다. 메세지 루프 안에서 GetMessage 이 만약 루프를 끝내고 프로그램을 종료하라는 WM_QUIT 메세지를 받으면 FALSE 를 리턴합니다.
TranslateMessage 는 로우한(raw) 키보드 입력을 받아, 메세지 큐에 새로운 메세지(WM_CHAR)를 만들어 넣는데 유용한 함수입니다. 이 메세지는 키가 눌러진 것을 처리하기 위해서 로우한 키보드 스캔 코드를 직접 다루지 않고도, 보다 쉽도록 하기 위해서 ASCII 값을 포함하고 있습니다. 만약 프로그램에서 키 입력을 사용하지 않을 것이라면, 이 함수 호출을 하지 않아도 됩니다.
DispatchMessage 는 메세지 처리할 특정 윈도우의 윈도우 프로시저에게 메세지 데이터를 전달 합니다.
 

  mov     eax,msg.wParam
  ret
WinMain endp

메세지 루프를 종료하면, MSG 구조체의 wParam 에 종료코드(exit code)가 저장됩니다. Windows 에게 이 종료코드(exit code)를 리턴하기 위해서는 eax 에 저장하면 됩니다. 오늘날에 Windows 는 리턴값을 사용하지 않지만, 이것이 더 안전하고 규칙에 의한 실행면에서 더 좋습니다.
 

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

이것은 우리의 윈도우 프로시져입니다. 꼭 WndProc 라는 이름을 사용해야 하는 것은 아닙니다. 첫번째 파라미터 hWnd 는 메세지가 도착해야 할 윈도우(창)의 윈도우 핸들입니다. uMsg 는 메세지입니다. uMsg 는 MSG 구조체가 아니라는 것을 명심하십시오. 이것은 단순히 숫자(number)입니다. Windows 수백개의 메세지를 정의하고 있습니다. 아마 이 중에서 대부분의 메세지는 여러분의 프로그램에서 사용하지 않을 것입니다. Windows 는 적당한 메세지를 전달해서 윈도우가 특정 동작들을 하도록 할 것입니다. 윈도우 프로시져는 이 메세지를 받아 반응합니다. wParam 과 lParam 은 어떤 메세지들에서 사용하는 추가적인(Extra) 파라미터입니다. 어떤 메세지들은 단순한 메세지 뿐 아니라 추가적인 데이터를 함께 전달합니다. 이런 데이터들은 lParam 과 wParam 을 통해서 윈도우 프로시저로 전달됩니다.


.IF uMsg==WM_DESTROY
   invoke PostQuitMessage,NULL
.ELSE
   invoke DefWindowProc,hWnd,uMsg,wParam,lParam
   ret
.ENDIF 
   xor eax,eax 
   ret
WndProc endp

여기가 아주 중요한 부분입니다. 대부분의 프로그램 동작방식(intelligence)이 들어있는 부분입니다. 각각의 Windows 메세지에 대해서 응답하는 코드가 들어있는 윈도우 프로시져입니다. 이 코드에서 프로그램이 처리해야 하는 메세지가 있는지 확인해야 합니다. 만약 처리해야 할 메세지를 발견하면 그 메세지에 대해서 처리하고 싶은 동작을 하고, eax 에 0을 넣어 리턴합니다. 만약 아니라면, 일반적인(default) 처리를 위해서 반드시 받은 파라미터를 그대로 사용해서 DefWindowProc 를 호출해야 합니다. DefwindowProc 는 여러분의 프로그램에서 처리하지 않는 메세지들을 처리할 API 함수입니다.
프로그램이 반드시 처리해야 할 메세지는 WM_DESTROY 입니다. 이것은 윈도우(창)가 닫힐 때, 윈도우 프로시져로 전달되는 메세지입니다. 윈도우(창)가 이 메세지를 받을 때는, 이미 윈도우가 스크린에서 사라진 상태입니다. 이것은 단지 윈도우가 종료되었음을 통보해 주는 것 뿐이며, Windows 로 돌아갈 준비를 해야 합니다. 이 메세지에 대한 행동으로는 Windows 로 돌아가기 전에 처리 해야할 잡다한 것들을 하는 정도 입니다. 이 상황에서는 프로그램을 종료하는 것 빼곤 달리 선택할 것이 없습니다. 만약 사용자가 윈도우를 닫는 것을 막고 싶다면, WM_CLOSE 메세지에서 처리해야만 합니다. 다시 WM_DESTORY 이야기로 돌아와서, 잡일을 처리하고 나서는 PostQuitMessage 를 호출해서 WM_QUIT 메세지가 모듈로 전달 되도록 해야 합니다. WM_QUIT 는 메세지 루프에서 빠져 나와 Windows 로 돌아갈 때, GetMessage 가 eax 에 0 을 넣어 전달하도록 할 것입니다. DestroyWindow 함수를 호출해서 WM_DESTORY 메세지가 자신의 윈도우 프로시저로 전달 되도록 할 수도 있습니다.



정말 이번 번역은 너무나 길었고 개인적으로 바빠서 오랜 시간이 걸렸습니다. ㅠ.ㅜ 그리고 너무나 원문이 앞뒤 문맥이 부드럽게 이어지지 않고 끊어져서 번역에 애를 먹었네요. 참, 이번 튜토리얼은 긴 만큼 윈도우 프로그래밍(Win32 API 를 이용한 윈도우)을 해 보지 않으신 분은 이해하는데 어려울 수도 있겠습니다. 하지만 천천히 보시면 전혀 어려울게 없답니다. 그리고 다음 튜토리얼부터는 이렇게 길지 않으니 겁먹지 마시고 천천히 봐 주시기 바랍니다. 그럼 다음 튜토리얼에서 뵙겠습니다. ^-^

p.s - 벚꽃놀이 잘 다녀 오세요~ ㅠ.ㅜ
신고