2007. 2. 16. 13:26
DevX Win32 Assembly Tutorial 18: Common Controls

Tutorial 18: Common Controls

이 번장에서는 공통컨트롤의 사용법에 대해서 설명한다. 레퍼런스가 되길 바라지만 간단히 설명하는 것을 이해해 주기 바란다.
 소스    실행 결과  

Theory:

Windows9x는 Windows3.X의 유저 인터페이스로부터 확장된 것이고 보다 풍부한 GUI를 표현 할 수 있다. 이것은, 상태바나 툴바같은 것들로서, Windows9x가 출시되기 전에 자주 사용되긴 했지만, 이것은 프로그래머가 직접 생성해서 사용했었다. 하지만 현재는, Windows9x, XP에는 이미 포함되어 있으므로, 이번장에서는 이런 공통 컨트롤에 대해서 알아보자.

다음은 새로운 컨트롤들이다.

  • 툴바
  • 툴팁
  • 상태바
  • 프로퍼티시트
  • 프로퍼티 페이지
  • 트리뷰
  • 리스트뷰
  • 애니메이션
  • 드래그 리스트
  • 헤더
  • 핫키
  • 이미지 리스트
  • 프로그래스바
  • 리치 에디트
  • 트랙바
  • 업다운

이들 모두를 메모리에 로드하는 것은 리소스의 낭비다. 리치 에디트 컨트롤을 제외하고는 모두 comctl32.dll 에 저장되어 있고,공통컨트롤을 사용할 때에 어플리케이션에서 로드해서 사용하면 된다. 리치 에디트 컨트롤은 richedXX.dll 에 저장되어 있다. 따로 존재하는 이유는, 리치 에디트 컨트롤이 매우 복잡하고, 코드가 매우 크기 때문이다. (추후에 따로 다루게 된다)

컨트롤이 저장되어 있는 comctl32.dl을를 로드할려면 ,InitCommonControls함수를 사용하면 된다. InitCommonControls 함수는 comctl32.dll에 속해 있는 함수로서, 함수의 참조가 프로그램중의 어딘지를 조사해주고, PE로더가 프로그램 실행시에 comctl32.dll를 로드해 준다. 다만, 이 함수를 실행할 필요가 없고, 단지 소스 코드의 어디엔가 있기만 하면 된다. 이 함수는 아무일도 하지 않고,단지 「ret 명령어」만 있기 때문이다. 이 함수의 목적은, 임포트 섹션에서 comctl32.dll의 참조를 얻는것이므로, 결과적으로, PE로더는 프로그램이 로드 되면 항상 comctl32.dll를 로드하게 된다. 실제로 중요한 함수는 DLL이 로드 되었을 때, 모든 공통컨트롤 클래스를 등록하는 DLL 엔트리 포인트 함수다. 공통컨트롤은 공통컨트롤 클래스를 기본으로 해서 생성되어 있고, 이것은 정확히, 차일드 윈도우 컨트롤의 에디트 박스나, 리스트 박스 등과 같다.

리치 에디트는 지금까지의 예와는 틀리게 사용되며, LoadLibrary 함수를 호출해서 명시적으로 로드해야만 하며, 언로드 또한 FreeLibrary 함수를 호출해서 프로그래머가 명시적으로 처리해 주어야만 한다.(33장에서 다시 설명한다)

실제로 생성하는것을 설명한다. 먼저, 리소스 에디터로 다이얼로그 박스에 컨트롤을 추가하거나 혹은 직접 리소스 스크립트를 생성한다. 대부분의 공통 컨트롤은, 컨트롤 클래스명을 인수로 해서 CreateWindowEx 함수나 CreateWindow 함수를 호출해서 생성한다. 이 중에는, 독자적인 함수를 사용해야만 하는것도 있지만,이것은 단지, CreateWindowEx 함수를 사용하기 쉽게 재포장 한 것이다. 이런 함수는 다음과 같은 것들이 있다.

  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create

공통 컨트롤을 생성하기 위해서,반드시  클래스명을 알아야 한다. 클래스명은 다음과 같다.

Class Name Common Control
ToolbarWindow32 Toolbar
tooltips_class32 Tooltip
msctls_statusbar32 Status bar
SysTreeView32 Tree view
SysListView32 List view
SysAnimate32 Animation
SysHeader32 Header
msctls_hotkey32 Hot-key
msctls_progress32 Progress bar
RICHEDIT Rich edit
msctls_updown32 Up-down
SysTabControl32 Tab

프로퍼티 시트와 프로퍼티 페이지, 이미지 리스트는 독자적으로 사용하는 생성 함수가 있고, 드래그 리스트 컨트롤은 리스트 박스를 확장한 것이므로, 별도의 클래스명은 존재하지 않는다. 이 표는 VC++의 리소스 에디터로 생성한 리소스 스크립트를 기반으로 해서 확인한 것이다. Borland제품이나 Charles Petzold의 클래스명과는 사뭇 다를수도 있지만, 이것은 정확한 것이다.

이런 공통 컨트롤은 WS_CHILD등의 일반적인 윈도우스타일을 사용할 수 있지만, 트리뷰컨트롤과 같이 전용의 TVS_XXXX 혹은, 리스트뷰컨트롤 전용의 LVS_XXXX등의 자신만을 위한 독자적인 스타일도 지정할 수 있다. 자세한것은 Win32API 레퍼런스를 참고하기 바란다.

이제부터는, 공통컨트롤과 부모윈도우의 통신방법에 대해서 설명한다. 차일드윈도우 컨트롤때와는 다르게, WM_COMMAND 메세지로서 통신할 수 없다. 그 대신에, 공통컨트롤에서 어떤 이벤트가 발생하면, WM_NOTIFY 메세지를 전송하게 되어 있다. 부모윈도우는, 공통컨트롤에 메세지를 보내서 제어하지만, 각각의 공통컨트롤마다 독자적인 메세지가 있으므로, 자세히 알고 싶다면, Win32API 레퍼런스를 참고하기 바란다.

예제로, 프로그레스바와 상태바를 사용하는 프로그램을 생성해 보겠다.

Sample code:

.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

.const
IDC_PROGRESS equ 1           ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass", 0
AppName   db "Common Control Demo", 0
ProgressClass db "msctls_progress32", 0      ; the class name of the progress bar
Message db "Finished! ", 0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance, eax
   invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT
   invoke ExitProcess, eax
   invoke InitCommonControls

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, CW_USEDEFAULT, CW_USEDEFAULT, 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, NULL, ADDR ProgressClass, NULL,\
           WS_CHILD+WS_VISIBLE, 100,\
           200,300,20, hWnd, IDC_PROGRESS,\
           hInstance, NULL
       mov hwndProgress, eax
       mov eax, 1000              ; the lParam of PBM_SETRANGE message contains the range
       mov CurrentStep, eax
       shl eax, 16                  ; the high range is in the high word
       invoke SendMessage, hwndProgress, PBM_SETRANGE, 0, eax
       invoke SendMessage, hwndProgress, PBM_SETSTEP, 10,0
       invoke CreateStatusWindow, WS_CHILD+WS_VISIBLE, NULL, hWnd, IDC_STATUS
       mov hwndStatus, eax
       invoke SetTimer, hWnd, IDC_TIMER, 100, NULL       ; create a timer
       mov TimerID, eax
   .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage, NULL
       .if TimerID! =0
           invoke KillTimer, hWnd, TimerID
       .endif
   .elseif uMsg==WM_TIMER       ; when a timer event occurs
       invoke SendMessage, hwndProgress, PBM_STEPIT, 0,0 ; step up the progress in the progress bar
       sub CurrentStep, 10
       .if CurrentStep==0
           invoke KillTimer, hWnd, TimerID
           mov TimerID, 0
           invoke SendMessage, hwndStatus, SB_SETTEXT, 0, addr Message
           invoke MessageBox, hWnd, addr Message, addr AppName, MB_OK+MB_ICONINFORMATION
           invoke SendMessage, hwndStatus, SB_SETTEXT, 0,0
           invoke SendMessage, hwndProgress, PBM_SETPOS, 0,0
       .endif
   .else
       invoke DefWindowProc, hWnd, uMsg, wParam, lParam
       ret
   .endif
   xor eax, eax
   ret
WndProc endp
end start

Analysis:

invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT
invoke ExitProcess, eax
invoke InitCommonControls

보는바와 같이 ExitProcess 함수의 뒤에서 InitCommonControls 함수를 호출 하고 있다. 이것은, 임포트 섹션에 comctl32.dll의 참조를 추가하기 위한 것으로, 실제로 InitCommonControls 함수가 실행되지 않더라도 공통컨트롤은 동작한다.

.if uMsg==WM_CREATE
     invoke CreateWindowEx, NULL, ADDR ProgressClass, NULL,\
        WS_CHILD+WS_VISIBLE, 100,200,300,20, hWnd, IDC_PROGRESS, hInstance, NULL
    mov hwndProgress, eax

이 부분이 공통 컨트롤을 생성하는 부분이다. 여기서 호출하는 CreateWindowEx 함수는 부모윈도우의 핸들로 사용되는 hWnd와 컨트롤을 구별하기 위해서 사용되는 컨트롤ID를 인수로서 사용하는 것을 주의하기 바란다. 하지만, 컨트롤의 윈도우핸들이 있으므로, 이 ID는 사용하지 않는다. 또한 모든 차일드 윈도우 컨트롤은 WS_CHILD 스타일이 지정되어야만 한다.

mov eax, 1000
mov CurrentStep, eax
shl eax, 16
invoke SendMessage, hwndProgress, PBM_SETRANGE, 0, eax
invoke SendMessage, hwndProgress, PBM_SETSTEP, 10,0

프로그레스바가 생성된 뒤에는, 범위 영역을 설정한다. 기본값은 0 에서 100 이다. 이 값을 변경하고 싶다면, PBM_SETRANGE 메세지로서 변경할 수 있다. 이 메세지의, lParam에 범위를 설정하지만, 상위워드에 최대값을, 하위워드에 최소값을 설정하게 된다. PBM_SETSTEP 메세지로, 증가값을 지정할 수도 있다. 예제에는 10 단위 만큼 증가하도록 설정했다. 이로인해, PBM_STEPIT 메세지를 전송하게되면, 프로그레스바의 인디케이터(indicator)가 10씩 증가하게 되는 것이다. 물론, 인디케이터(indicator)를 증가에 대한 상대지정 뿐만이 아니라, PBM_SETPOS 메세지를 사용해서, 절대지정도 가능하다. 이 메세지를 사용해서, 프로그레스바를 마음대로 다룰 수가 있다.

invoke CreateStatusWindow, WS_CHILD+WS_VISIBLE, NULL, hWnd, IDC_STATUS
mov hwndStatus, eax
invoke SetTimer, hWnd, IDC_TIMER, 100, NULL       ; create a timer
mov TimerID, eax

다음으로, CreateStatusWindow 함수를 사용해서 상태바를 생성한다. 이 함수는 아주 간단해서 설명할 것이 별로없다. 예제에서는, 100 밀리 세컨드마다 프로그레스바를 업데이트하므로, 상태바를 생성한 후, 타이머를 생성해야 한다. 타이머를 생성하기 위해서, SetTimer 함수를 호출하면 된다.

SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD

  • hWnd
    부모윈도우 핸들
  • TimerID
    타이머 ID( 0 이아닌값을 지정)
  • TimerInterval
    타이머 메세지가 생성되는 간격을 밀리세컨드 단위로 지정(1000ms = 1 Second)
  • lpTimerProc
    타이머 함수의 포인터, TimerInterval로 설정된 시간이 되면 이 함수가 호출된다. NULL이면,부모윈도우로 WM_TIMER 메세지를 전송한다.(우선순위가 높으므로 정확한계산수행)

함수가 성공적으로 수행되면, TimerID가 반환되고,실패하게 되면 0 이 반환된다. 이렇기 때문에, 타이머 ID를 0 이외의 숫자로 설정해야 하는 것이다.

.elseif uMsg==WM_TIMER
    invoke SendMessage, hwndProgress, PBM_STEPIT, 0,0
    sub CurrentStep, 10
    .if CurrentStep==0
        invoke KillTimer, hWnd, TimerID
        mov TimerID, 0
        invoke SendMessage, hwndStatus, SB_SETTEXT, 0, addr Message
        invoke MessageBox, hWnd, addr Message, addr AppName, MB_OK+MB_ICONINFORMATION
        invoke SendMessage, hwndStatus, SB_SETTEXT, 0,0
        invoke SendMessage, hwndProgress, PBM_SETPOS, 0,0
    .endif

인터벌 타임에 도달하게되면, 타이머는 WM_TIMER 메세지를 전송한다. 그래서, WM_TIMER 섹션에 실행하고 싶은 코드를 사용하면 되지만, 예제에는, 프로그레스바를 업데이트하고, 최대값에 도달했는지를 체크하는 코드로 되어있다. 만약 최대값이 되면, 타이머를 중지하고, SB_SETTEXT 메세지를 전송해서 상태바에 문자열을 설정한다. 메시지 박스가 표시되고 유저가 OK를 선택하면, 상태바의 문자열과 프로그레스바의 인디케이터(indicator)가 클리어 된다.


Posted by openserver