2007. 2. 16. 14:04
DevX Win32 Assembly Tutorial 22: SuperClassing

Tutorial 22: SuperClassing

이 번장에서는 슈퍼 클래싱이 무엇이며 그 사용법에 대해서 설명한다. 또한 탭 콘트롤에 대해서도 설명한다.
 소스    실행 결과  

Theory:

오랜시간 프로그램을 작성하다 보면, 자신이 필요한 용도의 컨트롤이 필요하게 된다는 것을 느끼게 될 것이다. 예를 들면, 숫자만 입력할 수 있는 10개의 에디트 컨트롤이 필요한 경우등이다, 이럴경우 해결책으로는 몇가지가 있다.

  • 자신만의 윈도우 클래스를 생성해서, 컨트롤을 인스턴스화한다.
  • 에디트 컨트롤을 생성하고, 그들을 모두 서브 클래싱해서 사용한다.
  • 에디트 컨트롤을 슈퍼 클래싱한다

첫번째 방법은 너무나 번거로운 방법이다. 프로그래머가 모든 기능을 구현해야 하기 때문이다.두번째 방법은, 편하기는 하지만, 그래도 여전히 번거롭다. 서브 클래싱해야 하는 컨트롤의 수가 적다면, 이것이 해결책이 되지만 여러개의 컨트롤을 서브클래싱하는 것은 효율적이지 않다. 이와 같은 경우는 슈퍼 클래싱이 아주 유용할 것이다.

슈퍼 클래싱은 특정의 윈도우 클래스를 「변경한다」는 방법이다. 「변경한다」 의미는, 어떤 목적을 위해서 윈도우 클래스의 속성을 변경해서, 컨트롤을 생성한다는 뜻이다.

슈퍼 클래싱의 순서는 다음과 같다.

  1. GetClassInfoEx 함수를 호출해서 슈퍼 클래싱하고 싶은 윈도우의 클래스정보를 얻는다. GetClassInfoEx 함수가 성공적으로 수행되면, 해당 윈도우 클래스에 관한 정보를 설정한 WNDCLASSEX 구조체의 포인터를 반환한다.
  2. WNDCLASSEX 멤버를 변경한다. 하지만, 변경하는 것은 2개의 멤버 뿐이다.
    • hInstance
      프로그램의 인스턴스 핸들을 지정한다
    • lpszClassName
      새로운 클래스명의 포인터를 지정한다

    lpfnWndProc 멤버는 변경하지 않지만, 대부분, 변경할 필요가 있다. 단지, CallWindowProc 함수로 원래의 윈도우 프로시저를 호출할려면, 변경 전의 원래의 lpfnWndProc의 값을 저장해두어야만 한다.
  3. WNDCLASSEX 구조체를 등록하고, 새로운 윈도우 클래스를 얻는다.
  4. 새로운 윈도우 클래스로부터 윈도우를 생성한다.

슈퍼 클래싱은 같은 속성의 컨트롤을 여러개 생성해야 할 경우 서브클래싱보다 편리한 방법이다. 곧바로 예제를 살펴보면 알 수 있을 것이다.

Example:

.386
.model flat, stdcall
option casemap:none
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

WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD
EditWndProc PROTO :DWORD, :DWORD, :DWORD, :DWORD

.data
ClassName db "SuperclassWinClass", 0
AppName   db "Superclassing Demo", 0
EditClass db "EDIT", 0
OurClass db "SUPEREDITCLASS", 0
Message db "You pressed the Enter key in the text box! ", 0

.data?
hInstance dd ?
hwndEdit dd 6 dup(? )
OldWndProc dd ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance, eax
   invoke WinMain, hInstance, NULL, NULL, 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_APPWORKSPACE
   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, WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT, ADDR ClassName, ADDR AppName,\
       WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, CW_USEDEFAULT,\
          CW_USEDEFAULT, 350,220, NULL, NULL,\
          hInst, NULL
   mov hwnd, eax

   .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 uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL wc:WNDCLASSEX
   .if uMsg==WM_CREATE
       mov wc.cbSize, sizeof WNDCLASSEX
       invoke GetClassInfoEx, NULL, addr EditClass, addr wc
       push wc.lpfnWndProc
       pop OldWndProc
       mov wc.lpfnWndProc, OFFSET EditWndProc
       push hInstance
       pop wc.hInstance
       mov wc.lpszClassName, OFFSET OurClass
       invoke RegisterClassEx, addr wc
       xor ebx, ebx
       mov edi, 20
       .while ebx<6
           invoke CreateWindowEx, WS_EX_CLIENTEDGE, ADDR OurClass, NULL,\
                WS_CHILD+WS_VISIBLE+WS_BORDER, 20,\
                edi, 300,25, hWnd, ebx,\
                hInstance, NULL
           mov dword ptr [hwndEdit+4*ebx], eax
           add edi, 25
           inc ebx
       .endw
       invoke SetFocus, hwndEdit
   .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage, NULL
   .else
       invoke DefWindowProc, hWnd, uMsg, wParam, lParam
       ret
   .endif
   xor eax, eax
   ret
WndProc endp

EditWndProc PROC hEdit:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   .if uMsg==WM_CHAR
       mov eax, wParam
       .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
           .if al>="a" && al<="f"
              sub al, 20h
           .endif
           invoke CallWindowProc, OldWndProc, hEdit, uMsg, eax, lParam
           ret
       .endif
   .elseif uMsg==WM_KEYDOWN
       mov eax, wParam
       .if al==VK_RETURN
           invoke MessageBox, hEdit, addr Message, addr AppName, MB_OK+MB_ICONINFORMATION
           invoke SetFocus, hEdit
       .elseif al==VK_TAB
           invoke GetKeyState, VK_SHIFT
           test eax, 80000000
           .if ZERO?
               invoke GetWindow, hEdit, GW_HWNDNEXT
               .if eax==NULL
                   invoke GetWindow, hEdit, GW_HWNDFIRST
               .endif
           .else
               invoke GetWindow, hEdit, GW_HWNDPREV
               .if eax==NULL
                   invoke GetWindow, hEdit, GW_HWNDLAST
               .endif
           .endif
           invoke SetFocus, eax
           xor eax, eax
           ret
       .else
           invoke CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
           ret
       .endif
   .else
       invoke CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
       ret
   .endif
   xor eax, eax
   ret
EditWndProc endp
end start

Analysis:

이 프로그램은 작업영역에 6개의 「사용자 정의된」에디트 컨트롤을 포함한 단순한 형태의 윈도우를 생성한다. 에디트 컨트롤은 16진수 문자만을 입력 받는 기능을 한다. 실제로, 서브 클래싱의 예제를 슈퍼 클래싱용으로 변경한 것이다. 일반적인 프로그램과 같지만, 흥미로운 부분은, 메인 윈도우가 생성되는 곳이다.

.if uMsg==WM_CREATE
    mov wc.cbSize, sizeof WNDCLASSEX
    invoke GetClassInfoEx, NULL, addr EditClass, addr wc

먼저, 슈퍼 클래싱하고 싶은 클래스의 데이터로 WNDCLASSEX 구조체를 설정해야 한다. 예제는, EDIT 클래스가 된다. GetClassInfoEx 함수를 호출하기 전에 WNDCLASSEX의 멤버변수 cbSize를 설정하는것을 잊지 말기 바란다. 그렇지 않으면, WNDCLASSEX 구조체의 데이터를 설정할 수 없다. GetClassInfoEx 함수로돌아와서, 변수 wc에는 새로운 윈도우 클래스를 생성하는데 필요한 정보가 설정되어 있다.

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName, OFFSET OurClass

wc의 멤버중에서 변경해야 하는 것이 있다. 가장 먼저 윈도우 프로시저의 포인터다. 원래의 윈도우 프로시저가 필요 하기때문에, 원래의 윈도우 프로시저의 포인터를 저장해 두고,, CallWindowProc 함수를 사용해서 원래의 윈도우 프로시저를 호출할 수 있다. 이 테크닉은 SetWindowLong 함수를 호출하지 않고도 직접 WNDCLASSEX 구조체를 수정한다는 것을 제외하면, 서브 클래싱때에 사용한 테크닉과 같은 것이다.

다음으로 2개의 멤버 hInstance 와 lpsClassName는 반드시 변경해야 한다.변경하지 않게되면 새로운 윈도우 클래스를 등록할 수 없게된다. 그렇기 때문에 자신의 프로그램의 hInstance와 새로운 클래스명을 변경한다.

invoke RegisterClassEx, addr wc

이제 준비가 되었으므로, 새로운 윈도우 클래스를 등록하고, 새로운 클래스를 얻는다.

xor ebx, ebx
mov edi, 20
.while ebx<6
    invoke CreateWindowEx, WS_EX_CLIENTEDGE, ADDR OurClass, NULL,\
         WS_CHILD+WS_VISIBLE+WS_BORDER, 20,\
         edi, 300,25, hWnd, ebx,\
         hInstance, NULL
    mov dword ptr [hwndEdit+4*ebx], eax
    add edi, 25
    inc ebx
.endw
invoke SetFocus, hwndEdit

클래스를 등록하고, 그 클래스를 기본으로해서 윈도우를 생성한다. 위의 코드는, ebx 레지스터를 생성하는 윈도우의 숫자를 세기 위한 용도로 사용하고 있다. edi 레지스터는 윈도우의 y좌표로서 사용되고 있고, 윈도우가 생성될때, 핸들은 배열에 저장된다. 모든 윈도우가 생성되면, 가장 처음의 윈도우에 포커스를 설정 한다.

이 시점에서, 16 진수 문자만을 받아들이는 에디트 컨트롤을 6개 생성하고, 윈도우 프로시저에서는 유저의 입력을 필터링 하고 있다. 실제로, 서브 클래싱의 예제와 같은 윈도우 프로시저로서, 서브 클래싱하는 것보다 더 많은 코드를 기록해야하는 일은 없다.

예제에서는, 좀더 변화를 주기위해, 탭컨트롤을 사용하고 있다. 보통, 다이얼로그 박스에 컨트롤을 배치할 때, 다이얼로그 박스 매니저는 유저가 탭키를 누르면, 다음 컨트롤로 포커스를 이동시키고, 쉬프트+탭키를 누르면, 이전의 컨트롤로 포커스를 옮겨 준다. 하지만 이런 기능은 표준 윈도우에 있는 컨트롤에서만 이용할 수 있는 것이다. 그렇기 때문에, 서브 클래싱을 했을 경우 이와 같은 처리를 직접해주어야 한다.

.elseif al==VK_TAB
    invoke GetKeyState, VK_SHIFT
    test eax, 80000000
    .if ZERO?
        invoke GetWindow, hEdit, GW_HWNDNEXT
        .if eax==NULL
            invoke GetWindow, hEdit, GW_HWNDFIRST
        .endif
    .else
        invoke GetWindow, hEdit, GW_HWNDPREV
        .if eax==NULL
            invoke GetWindow, hEdit, GW_HWNDLAST
        .endif
    .endif
    invoke SetFocus, eax
    xor eax, eax
    ret

위의 코드는, EditWndProc 프로시저로부터, 유저가 탭 키를 눌렀는지를 체크하고, 눌렀다면 GetKeyState 함수로 쉬프트 키가 눌려진지도 체크하고있다. GetKeyState 함수는 지정된 키가 눌려져 있는지를 검사해서 결과를 eax 레지스터에 반환한다. 만약 키가 눌렸다면, eax 레지스터의 상위비트가 설정 되고,만약 눌려지지 않았다면, 상위 비트가 클리어되어있게 된다. 그렇기 때문에 test명령어를 사용해서, 80000000 h와 비교하고 있다. 만약 상위 비트가 설정되면, 쉬프트+탭키가 눌려진 경우이고, 이경우는 다른 처리가 필요하게 된다.(이전 컨트롤로 이동)

유저가 탭 키만 눌렀을 경우, GetWindow 함수로 다음 컨트롤의 핸들을 얻는다.이때, 현재 에디트 컨트롤 hEdit 로부터 다음의 윈도우를 얻을수 있도록 하기위해, GW_HWNDNEXT 플래그를 사용해서 GetWindow 함수를 호출한다. 이 함수가 NULL을 반환하면, 현재 컨트롤이 가장 마지막 컨트롤이란 의미이고, GetWindow 함수를 GW_HWNDFIRST 플래그를 지정해서 호출함으로써 처음 컨트롤을 얻는다. 쉬프트+탭의 경우는, 탭 키가 눌렸을때의 반대로 작동하므로, 같은 것이다.


Posted by openserver