2007. 2. 16. 13:44
DevX Win32 Assembly Tutorial 20: Window Subclassing

Tutorial 20: Window Subclassing

이 번장에서는, 윈도우 서브클래싱에 대해서 설명한다.
 소스    실행 결과  

Theory:

Windows 프로그램을 만들었을 경우, 기본적인 기능을 수행할 수 있지만, 이미 만들어진 윈도우의 기능을 변경하고 싶은 경우가 발생할 수 있다. 예를 들어, 입력문자를 검사해서 불필요한 입력을 막을 수 있는 에디트 컨트롤을 갖고 싶다 라든지의 경우를 의미한다.
이런 경우에 프로그램을 다시 만들수도 있겠지만 너무 번거롭게 된다.이럴경우 윈도우 서브클래싱을 사용하면 된다. 서브클래싱이라는 것은, 서브 클래싱된 윈도우를 「제어」할 수 있게 만들어준다.

간단하게 설명하면, 16 진수 문자만 입력할 수있는 텍스트 박스를 사용하고 싶다고 가정하자. 단순히 에디트 컨트롤을 사용하는 경우, 유저가 텍스트박스에 16 진수 이외의 문자를 입력하는것을 막을 방법이 없다. 즉, 유저가"zb+q*"라고 입력하면, 유저가 입력한 것이 무엇인지를 체크해야한다. 실제로,유저가 입력하는 경우에 그값이 유효한 값인지를 체크 해 주어야 한다.

이 경우에, Windwos는 에디트 컨트롤의 윈도우 프로시저에게 WM_CHAR 메세지를 전송하지만, 윈도우 프로시저는 Windows내부에 있으므로, 우리가 어찌 할 수가 없다. 그렇지만, 직접 생성한 윈도우 프로시저에 대해서는 메세지를 재가공할 수 있다. 그렇기 때문에, Windows가 에디트 컨트롤에 전송하는 메세지를 자신의 윈도우 프로시저에서 먼저 처리를 하게 할 수 있다.이는 다음과 같이, 자신만의 윈도우 프로시저를 Windows와 에디트 컨트롤의 사이에 배치함으로써 원하는 업무를 처리 할 수 있다.

 Before Subclassing   Windows ==> 에디트 컨트롤의 윈도우 프로시저  
 After Subclassing   Windows ==> 자신의 윈도우 프로시저 ==> 에디트 컨트롤의 윈도우 프로시저  

그렇다면 어떻게 서브클래싱을 사용하면 되는지를 설명한다. 먼저, 서브 클래싱은 컨트롤에만 사용되는 것이 아니고 어떤 윈도우에도 적용할 수 있다. 그런데Windows는 에디트 컨트롤의 윈도우 프로시저가 어디에 있는지를 알고 있는 것일까?
... 잠시 예전에 설명했던 윈도우클래스를 생각해 보자. WNDCLASSEX 구조체에서 lpfnWndProc라는 멤버가 있었을 것이다. 만약 이 멤버의 값을 자신이 생성한 윈도우 프로시저로 변경할 수 있다면, Windows는 새로운 윈도우프로시저로 메세지를 전송하게 될 것이다.

이를 처리할려면 , SetWindowLong 함수를 호출하면 된다.

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

  • hWnd
    WNDCLASSEX 구조체의 값을 변경할려는 윈도우의 핸들
  • nIndex
    WNDCLASSEX 구조체의 어떤값을 변경할지를 결정
    • GWL_EXSTYLE
      확장윈도우 스타일을 변경한다
    • GWL_STYLE
      윈도우 스타일을 변경한다
    • GWL_WNDPROC
      윈도우 프로시저를 변경한다
    • GWL_HINSTANCE
      어플리케이션의 인스턴스 핸들을 변경한다
    • GWL_ID
      윈도우의 ID를 변경한다
    • GWL_USERDATA
      윈도우에 대한 32비트의 파라미터를 변경한다. 윈도우는 생성되는 어플리케이션에서 사용되는 32 비트값을 설정한다.
  • dwNewLong
    변경된 값

해야 할일은 매우 간단하다. 에디트 컨트롤의 메세지를 처리하는 윈도우 프로시저를 변경하고 싶다면, 두번째의 인수인 GWL_WNDPROC을 설정하고, 세번째 인수에 새로운 윈도우 프로시저를 설정해서 SetWindowLong 함수를 호출하면 된다. 함수가 성공적으로 수행되면, 반환값으로 이전에 설정되어 있던 32 비트의 정수값이 반환된다. 예제에서는, 원래 윈도우 프로시저의 주소이다. 이 주소는 복구를 위해서 사용되므로 반드시 저장해 두어야 한다.

처리하고 싶은 메세지가 있듯이 처리하고 싶지 않은 메세지도 있을 것이다. 이런 메세지는 CallWindowProc 함수를 호출해서, 원래의 윈도우 프로시저에서 처리하게 할 수가 있다.

CallWindowProc PROTO lpPrevWndFunc:DWORD, \
                     hWnd         :DWORD, \
                     Msg          :DWORD, \
                     wParam       :DWORD, \
                     lParam       :DWORD

  • lpPrevWndFunc
    원래의 윈도우 프로시저의 주소
  • 나머지의 파라미터
    자신의 윈도우 프로시저에서 넘겨진 값을 그대로 사용

Example:

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD
EditWndProc PROTO :DWORD, :DWORD, :DWORD, :DWORD

.data
ClassName db "SubclassWinClass", 0
AppName   db "Subclassing Demo", 0
EditClass db "EDIT", 0
Message db "You pressed Enter in the text box! ", 0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
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, ADDR ClassName, ADDR AppName,\
 WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, CW_USEDEFAULT,\
          CW_USEDEFAULT, 350,200, 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 hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   .if uMsg==WM_CREATE
       invoke CreateWindowEx, WS_EX_CLIENTEDGE, ADDR EditClass, NULL,\
           WS_CHILD+WS_VISIBLE+WS_BORDER, 20,\
           20,300,25, hWnd, NULL,\
           hInstance, NULL
       mov hwndEdit, eax
       invoke SetFocus, eax
       ;-----------------------------------------
       ; Subclass it!
       ;-----------------------------------------
       invoke SetWindowLong, hwndEdit, GWL_WNDPROC, addr EditWndProc
       mov OldWndProc, eax
   .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
       .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:

invoke SetWindowLong, hwndEdit, GWL_WNDPROC, addr EditWndProc
mov OldWndProc, eax

에디트 컨트롤을 생성하고 나서, SetWindowLong 함수를 호출해서 서브 클래싱을 실행한다. 이로 인해, 원래의 윈도우 프로시저와 자신의 윈도우 프로시저의 주소가 바뀌게 되지만, CallWindowProc 함수를 호출하기 전까지는 자신의 윈도우 프로시저를 사용하게 되므로, 원래의 윈도우 프로시저의 주소를 반드시 저장해 두어야 이것이 가능해진다. 또한, EditWndProc는 매우 일반적인 윈도우 프로시저이다.

.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

EditWndProc 함수에서, WM_CHAR 메세지에 대한 처리를 하고 있다. 만약 입력된 문자가 0-9 또는 a-f 사이라면, 원래의 윈도우 프로시저에 처리를 하게 한다. 이 때, 입력된 캐릭터가 소문자라면 0x20을 더해서 대문자로 변경해주고 있다. 여기에서 원하지않는 문자가 입력된다면 원래의 윈도우 프로시저에서 처리하지 않게 한다. 이로 인해, 유저가 0-9 와 a-f 문자가 아닌 것을 입력해도 에디트 박스에는 표시하지 않게 된다.

.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
  .else
    invoke CallWindowProc, OldWndProc, hEdit, uMsg, wParam, lParam
    ret
  .end

이 예제에서는, 엔터키를 트랩 함으로써, 서브 클래싱의 새로운 면을 보여주고 있다. EditWndProc 함수에서 WM_KEYDOWN 메세지가 전송될때, VK_RETURN(리턴 키를 나타내는 가상키코드) 인지를 체크하고 있다. 만약 리턴 키라면, 메시지 박스를 표시하고, 엔터키가 아니라면, 원래의 윈도우 프로시저로 메세지를 전송한다.

이와 같이, 서브 클래스싱을 사용해서 윈도우를 다룰 수가 있다. 반드시 이 기술을 익혀 두기 바란다.


Posted by openserver