2007. 2. 21. 17:19
DevX Win32 Assembly Tutorial 31: ListView Control

Tutorial 31: ListView Control

이번에는 리스트뷰 컨트롤에 대한 설명이다. 내용이 조금 많으나 천천히 익혀가길 바란다.
 메인 소스    헤더 파일    리소스    실행 결과  

Theory:

리스트뷰 컨트롤은 트리뷰나 리치 에디트와 같은 공통 컨트롤의 일종이다. 예를 들면, 탐색기의 오른쪽 부분이 리스트뷰 컨트롤이다(트리뷰와 혼동하지 말기 바란다). 리스트뷰 컨트롤은 아이템을 표시하는데 적절한 컨트롤로서, 어떻게 보면, 리스트 박스와 유사하지만 더욱 강력한 기능들이 있다.

리스트뷰 컨트롤을 생성 방법은 두가지 있다. 첫번째 방법은 매우 간단하다, 리소스 에디터로 생성하는 것이다. 이 경우, InitCommonControls함수를 호출하는 것을 절대 잊지 말기 바란다.
또다른 방법은 CreateWindowEx함수를 호출해서,윈도우 클래스명을 SysListView32로 설정해서 생성하는 것이다. "WC_LISTVIEW"라는 윈도우 클래스명은 쓰지말기바란다.

리스트뷰 컨트롤에서 데이터를 표시하는 방법은 「아이콘」 「작은 아이콘」 「리스트」 「리포트뷰」의 4가지다. 이런 표시 방법은 윈도우 탐색기를 실행해서 「icon view(Larage Icons)」 「small icon view(Small Icons)」 「(list view) List」 「(report view) Details」를 선택해 보면 알 수 있을것이다.
이들은 단순한 표시 방법에 따른 차이로서, 데이터는 동일한 것이다. 예를 들어, 리스트뷰 컨트롤에 대량의 데이터가 있을 경우, 그 중 필요한 데이터만 표시할 수가 있다. 추가로, 리포트뷰가 가장 표시되는 정보량이 많은 표시 방법이다. (탐색기에서는 "자세히보기"에 해당한다)
리스트뷰 컨트롤을 생성할 경우에 어떤 표시 방법을 사용하는지를 지정하게 되어 있지만, GWL_STYLE플래그를 지정해서 SetWindowLong함수를 호출해서도 변경할 수 있다.

리스트뷰 컨트롤의 생성 방법을 알았으니, 이제 다음으로 사용 방법에 대해 알아보자. 여기서는 리스트뷰 컨트롤의 많은 특징을 이해하기 위해서 가장 복잡한 리포트뷰에 대해서 설명 한다. 사용 방법은 다음과 같다.

  1. 클래스명을 SysListView32로 설정한 후 CreateWindowEx함수를 호출해서 리스트뷰 컨트롤을 생성한다. 이 때, 초기의 표시 방법을 지정할 수 있다.
  2. 만약에 리스트뷰아이템으로 사용하는 이미지가 있다면 생성 및 초기화를 수행한다.
  3. 리스트뷰 컨트롤이 리포트뷰를 사용한다면, 리스트뷰 컨트롤에 행(컬럼)을 추가한다.
  4. 리스트뷰 컨트롤에 아이템과 서브 아이템을 삽입한다.

●Columns

리포트뷰에는 1개 이상의 행이 있다. 리포트뷰의 데이터는 테이블과 같이 생각할 수 있으므로, 이런 데이터는 열과 행을 지정해서 편집하게 된다. 리스트뷰 컨트롤에는 최소한 1행 이상이 존재하게 된다. 리포트뷰 이외의 표시 형식에는, 1행 밖에 필요하지 않기 때문에 반드시 행을 삽입할 필요는 없다.

리스트뷰에는LVM_INSERTCOLUMN을 전송하는 것으로 행을 삽입한다.

LVM_INSERTCOLUMN
  • wParam = iCol
  • lParam = LV_COLUMN구조체의 포인터

iCol는 0 부터 시작되는 행 번호다.
LV_COLUMN구조체는 컬럼 정보가 저장되어 있고 다음과 같이 정의 되어 있다.

LV_COLUMN STRUCT
  imask       dd ?
  fmt         dd ?
  lx          dd ?
  pszText     dd ?
  cchTextMax  dd ?
  iSubItem    dd ?
  iImage      dd ?
  iOrder      dd ?
LV_COLUMN ENDS

필드명 의미
imask

이 구조체의 어떤 멤버가 유효한지를 나타내는 플래그. 의미는, 구조체의 모든 멤버가 동시에 사용되는 것이 아니라는 것이다. 추가로 이 구조체는 입력 / 출력에도 사용된다.

  • LVCF_FMTfmt가 유효
  • LVCF_SUBITEMiSubItem이 유효
  • LVCF_TEXTpszText가 유효
  • LVCF_WIDTHlx가 유효

상기의 플래그는 조합해서 지정하는 것이 가능하다, 예를 들면 컬럼의 문자열을 기술하고 싶다면, pszText멤버에 문자열의 포인터를 설정한다. 그리고, pszText 멤버에 값이 설정되어 있다는 것을 명시하기 위해서, LVCF_TEXT플래그를 지정한다. 그렇지 않으면 Windows는 pszText의 값을 무시한다.

fmt 컬럼내의 아이템, 서브 아이템을 어떻게 배치하는지를 지정한다. 사용할 수 있는 값은 다음과 같다.
  • LVCFMT_CENTER:텍스트를 중앙정렬 한다
  • LVCFMT_LEFT:텍스트를 왼쪽정렬 한다
  • LVCFMT_RIGHT:텍스트를 오른쪽정렬 한다
lx 컬럼의 폭을 픽셀 단위로 지정한다. LVM_SETCOLUMNWIDTH 메세지를 사용해서 컬럼폭을 변경할 수 있다.
pszText 컬럼속성정보를 설정 하기 위한 용도로 사용되는 경우, 컬럼명의 포인터가 되고, 컬럼속성정보를 받기 위해 사용되는 경우는, 현재 설정된 컬럼명을 받기 위한 버퍼의 포인터가 된다. 이 경우, 다음에 나오는 cchTextMax변수에 버퍼의 사이즈를 지정한다. 컬럼명을 설정하는 경우는, 문자열의 길이는 알고 있으므로, cchTextMax 값은 무시된다.
cchTextMax pszText 변수의 크기를 바이트단위로 지정한다. 이 변수는,구조체가 컬럼 정보를 받는 때에만 사용된다. 이값을 설정하게 되면 이 값은 무시된다.
iSubItem

서브 아이템의 인덱스를 지정한다. 서브 아이템이 어떤 것인지를 구별하는 용도로 사용된다.
추가로, 컬럼 넘버와 일치하지 않는 값을 지정할 수도 있다.이 경우 리스트뷰 컨트롤은 정상 작동하지만 , 다시 컬럼 넘버를 구하고 싶을 때는 불가능하므로 자제하기 바란다.

iImage 이미지 리스트의 이미지 인덱스( 0 부터 시작).
iOrder 0부터 시작되는 컬럼 오프셋(offset)값. 순서는 왼쪽에서 오른쪽이다. 예를 들면 0 을 지정하면 제일왼쪽의 컬럼을 나타낸다.

리스트뷰 컨트롤를 생성 한 후, 반드시 1개이상의 컬럼을 추가해야 한다. (리포트뷰일 경우의 얘기이다.) 컬럼을 삽입하기 위해서, LV_COLUMN 구조체를 생성하고, 컬럼 넘버 등을 적당한 값으로 설정 할 필요가 있다. 그 후, LVM_INSERTCOLUMN메세지를 전송한다.

LOCAL  lvc:LV_COLUMN
mov    lvc.imask, LVCF_TEXT+LVCF_WIDTH
mov    lvc.pszText, offset Heading1
mov    lvc.lx, 150
invoke SendMessage, hList, LVM_INSERTCOLUMN, 0, addr lvc

상기의 코드는 개략을 나타낸 것이다. 컬럼 헤더 텍스트를 설정하고, LVM_INSERTCOLUMN메세지를 전송한다.

●Items and subitems

아이템은 리스트뷰 컨트롤의 주된 요소이다. 리포트뷰 외의 표시 방법 이라해도 , 리스트뷰 컨트롤의 아이템은 중요하다. 서브 아이템은 아이템의 상세한 정보를 보관 / 유지하고 있다. 아이템은 1개이상의 서브 아이템과 연계될 가능성이 있다, 예를 들어, 아이템에 파일명인 경우, 속성, 사이즈, 생성 일시 등을 서브 아이템으로서 보관 / 유지한다. 리포트뷰에서는 제일 왼쪽에 있는 것이 아이템이고, 나머지는 서브 아이템이다. 이들은 데이타베이스의 레코드와 아주 유사하다.

리스트뷰에 아이템을 표시하는 경우, 서브 아이템은 반드시 필요하지 않지만, 보다 사용자에게 알기 쉽게 하기 위해서는 있는 것이 좋을 것이다. 리스트뷰에 아이템을 삽입하려면 , 리스트뷰 컨트롤에 LVM_INSERTITEM 메세지를 전송하고, lParam 파라미터에는 LV_ITEM구조체의 포인터를 지정한다. LV_ITEM은 다음과 같이 정의되어 있다.

LV_ITEM STRUCT
  imask       dd ?
  iItem       dd ?
  iSubItem    dd ?
  state       dd ?
  stateMask   dd ?
  pszText     dd ?
  cchTextMax  dd ?
  iImage      dd ?
  lParam      dd ?
  iIndent     dd ?
LV_ITEM ENDS

필드명 의미
imask 어떤 멤버가 유효한지를 나타낸다. LV_COLUMN구조체의 imask 멤버와 같다.
iItem 0으로부터 시작되는 아이템 번호로서, 테이블의 "행" 과 유사하다.
iSubItem

iItem으로 지정된 아이템과 관련이 있는 서브 아이템의 인덱스값. 이 값은 테이블의 컬럼과 유사하다. 예를 들어, 새롭게 생성한 리스트뷰 컨트롤에 아이템을 삽입하고 싶다면, iItem의 값은 0 이 될 것이다 (이 아이템이 첫번째이므로).
그리고, iSubItem의 값 또한 0 이 된다(이 아이템을 첫번째 컬럼으로 하고 싶기 때문에). 아이템과 관련이 있는 서브 아이템이 있으면, iItem은 연관짓고 싶은 아이템의 인덱스값이 되고(여기에서는 0 이 된다),iSubItem는 1 이상의 값이 된다(어느 컬럼에 삽입할지에 따라 다르다). 예를 들면, 리스트뷰 컨트롤에 4줄의 컬럼이 있을 때, 최초의 컬럼은 아이템이고, 나머지 3개는 서브 아이템이 된다. 4번째의 컬럼에 서브 아이템을 삽입하고 싶으면, iSubItem은 3 이 된다.

state 아이템의 상태를 나타낸다. 아이템 상태는 사용자나 프로그램에 의해 변경할 수 있는 것으로, 포커스 상태, 하이라이트 상태, 선택 상태 등이 있다. 상태 플래그에 추가로, 1 로부터 시작되는 오버레이 이미지 인덱스값, 상태 이미지 인덱스값도 포함된다.
stateMask state멤버에 상태 플래그, 오버레이 이미지 인덱스값, 상태 이미지 인덱스값이 포함되어 있으므로, 값을 설정하고 싶은지 값을 얻기 위해서인지를 Windows에게 전달할 필요가 있으므로, 그 때에 사용한다.
pszText 아이템을 삽입할 때 아이템명으로 사용되는 문자열의 포인터. 아이템 속성을 얻기 위해서 이 구조체를 설정하는 경우는 아이템명을 보관 유지하기 위한 버퍼의 포인터가 된다.
cchTextMax 아이템 정보를 얻을때만 사용하며,pszText의 최대바이트를 나타낸다.
iImage 리스트뷰 컨트롤의 아이콘을 저장하고 있는 이미지 리스트의 인덱스값. 이 아이템이 사용하는 아이콘을 지정한다.
lParam

리스트뷰 컨트롤을 정렬 할 때에 사용하는 사용자가 정의하는 값. 아이템을 정렬할 때 아이템을 쌍으로 해 비교하지만, 쌍으로 되어 있는 양쪽 모두의 아이템의 lParam값이 전송되므로, 어느 쪽이 앞인지를 결정하면 좋다. 잠시 후에 정렬에 관해서보다 자세한 설명을 하므로, 그냥 넘어가자.

iIndent 아이템의 들여쓰기폭을 지정한다.

그럼 아이템, 서브 아이템을 리스트뷰 컨트롤에 삽입할 때의 순서를 정리해 보자.

  1. LV_ITEM구조체를 생성한다
  2. 구조체에 적절한 값을 설정한다
  3. 아이템을 삽입하고 싶은 경우는 LVM_INSERTITEM 메세지를 전송한다. 서브 아이템의 경우는, LVM_SETITEM 이다. 이는 아이템과 서브 아이템의 관계를 조금 까다롭게 할지도 모르지만, 서브 아이템은 아이템의 부가적인 정보라고 생각하면 이해하기 쉽다.

●ListView Messages/Notifications

어떻게 리스트뷰 컨트롤를 생성하는지를 알아 봤으니,다음으로 어떻게 상호 통신을 하는지를 설명한다.
리스트뷰 컨트롤과 부모윈도우는 메세지를 교환하는 것으로 통신을 한다. 즉, 부모윈도우는 리스트뷰 컨트롤에 메세지를 전송하고, 리스트뷰 컨트롤는 부모윈도우에 WM_NOTIFY메세지를 전송해서 메세지를 받은 것을 통지한다. 다른 공통 컨트롤과 방식이  같다.

●Sorting items/subitems

이번은 방금전 말했던 정렬에 관한 설명이다. CreateWindowEx함수를 호출 할 때에 LVS_SORTASCENDINGLVS_SORTDESCENDING을 지정하는 것으로, 리스트뷰 컨트롤의 디폴트 정렬순서를 결정할 수 있다. 이런 2개의 스타일은 아이템명 만 정렬하게 된다. 만약 다른 방법으로 정렬하고 싶다면, LVM_SORTITEMS 메세지를 리스트뷰 컨트롤에 전송할 필요가 있다.

LVM_SORTITEMS
  • wParam = lParamSort
  • lParam = pCompareFunction
  • lParamSort는 사용자가 정의하는 값으로 비교하는 함수에 전달된다.
  • pCompareFunction는 사용자가 정의하는 함수의 포인터로서, 아이템의 정렬순서를 결정한다. 이 함수는 다음과 같은 프로토타입이다.

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

  • lParam1와 lParam2 LV_ITEM구조체의 멤버 lParam의 값으로, 리스트뷰 컨트롤에 아이템을 삽입할 때 지정한다.
  • lParamSortLVM_SORTITEMS 메세지를 전송할 경우에 지정하는 wParam의 값

리스트뷰 컨트롤이 LVM_SORTITEMS 메세지를 받게되면, 2개의 아이템을 비교하기 위해서, lParam로 지정되어 있는 비교 함수를 호출한다. 즉, 비교 함수에 의해서, 2개의 아이템중 어느 쪽의 아이템이 앞에 올 지를 알게 된다. 법칙은 간단하다. 비교 함수가 거짓을 반환하면, 첫번째 아이템(lParam1)이 먼저오게 되고, 참을 반환하면 2번째의 아이템(lParam2)이 먼저오게 된다. 0 이 반환되면 같은 값이라고 판단한다.

이 방법에 정상적으로 작동할려면, LV_ITEM구조체의 lParam의 값이 중요하다. 아이템을 정렬 할 때, lParam의 값을 사용해서 정렬하게 된다.
예를 들어, lParam에 아이템의 인덱스값을 설정했다면, LVM_GETITEM 메세지를 전송해서 아이템 정보를 얻는다. 아이템이 변경됬을 때는 인덱스값도 바뀌게되므로, 정렬 할 수 없게 되어, 새로운 인덱스값을 lParam에 설정해야 한다.

사용자 컬럼 헤더를 클릭했을 때에 아이템을 정렬하고 싶다면, 윈도우 프로시저에서 LVN_COLUMNCLICK 메세지를 처리해야한다. LVN_COLUMNCLICKWM_NOTIFY메세지 통해서 전송된다.

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

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
template db "%lu",0
FileNameSortOrder dd 0
SizeSortOrder dd 0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu 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, NULL
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,IDM_MAINMENU
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

InsertColumn proc
LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc
or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT
mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc 
ret 
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE

mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem 
mov lvi.iSubItem,0
lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD
invoke FindFirstFile,addr FileNamePattern,addr finddata
.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi
.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?
invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw
invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
LOCAL Result:DWORD
mov Result,0
mov edi,String
invoke lstrlen,String
.while eax!=0
xor edx,edx
mov dl,byte ptr [edi]
sub dl,"0" ; subtrack each digit with "0" to convert it to hex value
mov esi,eax
dec esi
push eax
mov eax,edx
push ebx
mov ebx,10
.while esi > 0
mul ebx
dec esi
.endw
pop ebx
add Result,eax
pop eax
inc edi
dec eax
.endw
mov eax,Result
ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi
.elseif SortType==2
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub eax,edi
.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer 
.else
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer,addr buffer1
.endif
ret
CompareFunc endp

UpdatelParam proc uses edi
LOCAL lvi:LV_ITEM
invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
mov edi,eax
mov lvi.imask,LVIF_PARAM
mov lvi.iSubItem,0
mov lvi.iItem,0
.while edi>0
push lvi.iItem
pop lvi.lParam
invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
inc lvi.iItem
dec edi
.endw
ret
UpdatelParam endp

ShowCurrentFocus proc 
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE
invoke SendMessage,hList,LVM_GETNEXTITEM,-1,LVNI_FOCUSED
mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
invoke MessageBox,0, addr buffer,addr AppName,MB_OK
ret
ShowCurrentFocus endp


WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax
invoke InsertColumn
invoke FillFileInfo
RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK
mov edx,wParam
and edx,0FFFFh 
push edx
or eax,edx
invoke SetWindowLong,hList,GWL_STYLE,eax
pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif
.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList
.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW 
.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2
invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,1
.else
invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,2
.endif 
.else
.if FileNameSortOrder==0 || FileNameSortOrder==4
invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,3
.else
invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,4
.endif 
.endif
assume edi:ptr NMHDR
.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif
.endif
pop edi
.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam 
ret
.endif
xor eax,eax
ret
WndProc endp
end start
            

Analysis:

처음으로 리스트뷰 컨트롤를 생성하기 위해서 메인 윈도우를 생성한다.

.if uMsg==WM_CREATE
  invoke CreateWindowEx, NULL, addr ListViewClassName, NULL,\
         LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0, hWnd, NULL, hInstance, NULL
  mov hList, eax

윈도우 클래스명을"SysListView32"로 CreateWindowEx 함수를 호출한다. 디폴트 표시는 LVS_REPORT스타일을 지정해서 리포트뷰로 한다.

invoke InsertColumn

리스트뷰 컨트롤을 생성 후, 컬럼을 삽입한다.

LOCAL lvc:LV_COLUMN

mov lvc.imask, LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText, offset Heading1
mov lvc.lx, 150
invoke SendMessage, hList, LVM_INSERTCOLUMN, 0, addr lvc

파일명을 설정하기 위해서 LV_COLUMN구조체의 폭과 레이블을 사용하는 것을 선언하기 위해서, imask플래그에 LVCF_TEXT LVCF_WIDTH를 설정해야한다.
그 후, pszText에 레이블명의 주소를, lx에는 컬럼폭을 픽셀값으로 설정한다. 준비가 되면 설정한 구조체를 넘겨주기 위해, 리스트뷰 컨트롤에 LVM_INSERTCOLUMN 메세지를 전송한다.

or lvc.imask, LVCF_FMT
mov lvc.fmt, LVCFMT_RIGHT

컬럼삽입을 완료하면, 다음으로 파일 사이즈를 설정 하기 위해 컬럼을 하나 더 삽입한다. 사이즈 컬럼은 오른쪽으로 정렬 하고 싶기 때문에, fmt멤버에 LVCFMT_RIGHT를 지정하고,imask LVCF_FMT플래그를 지정한다.

mov lvc.pszText, offset Heading2
mov lvc.lx, 100
invoke SendMessage, hList, LVM_INSERTCOLUMN, 1 , addr lvc

나머지는 간단하다. pszText에 레이블의 포인터를 설정하고, lx에 폭을 설정한다. 그리고 컬럼 넘버와 이 구조체의 주소를 지정하고, LVM_INSERTCOLUMN메세지를 리스트뷰 컨트롤에 전송한다.

컬럼을 삽입할 때, 리스트뷰 컨트롤의 아이템을 설정 할 수 있다.

invoke FillFileInfo

FillFileInfo는 다음과 같다.

FillFileInfo proc uses edi
  LOCAL finddata:WIN32_FIND_DATA
  LOCAL FHandle:DWORD

  invoke FindFirstFile, addr FileNamePattern, addr finddata

FIndFirstFile 함수를 호출해서, 검색 문자열에 일치하는 파일 정보를 얻는다. FindFirstFile 함수는 다음과 같은 프로토타입을 가진다.

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

  • pFileName은 검색하고자 하는 파일명. 이 문자열은 와일드 카드 지정이 가능하다. 예제에서는 해당 폴더의 모든파일을 얻기위해"*. *"로 설정하고 있다.
  • pWin32_Find_Data WIN32_FIND_DATA구조체의 주소로서 파일 정보가 설정되게 된다.

해당되는 파일이 없다면 eax 레지스터에 INVALID_HANDLE_VALUE가 설정되고, 발견 된다면 FindNextFile함수를 호출할 때 필요한 검색용 핸들을 반환된다.

.if eax! =INVALID_HANDLE_VALUE
  mov FHandle, eax
  xor edi, edi

파일이 있으면, FHandle에 설정되고, 아이템의 인덱스(행 넘버)로서 사용하는 edi 레지스터를 0 으로 한다.

.while eax! =0
   test finddata.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY 
   .if ZERO? 

예제에서는, 폴더를 다루고 싶지 않기 때문에, dwFileAttributes FILE_ATTRIBUTE_DIRECTORY를 포함하고 있는지 체크해서 폴더를 제외시키고 있다.

      invoke ShowFileInfo, edi, addr finddata
      inc edi
   .endif
   invoke FindNextFile, FHandle, addr finddata    
. endw

ShowFileInfo 함수를 호출해서 파일명과 파일 사이즈를 리스트뷰 컨트롤에 삽입한다. edi 레지스터는 현재의 행 넘버이므로 증가시켜(increment) 둔다. 마지막에FindNextFile함수를 호출해서 다음 파일을 검색한다. FindNextFile이 0 을 반환할 때까지(즉 모든파일을 다 검색할 때까지 ) 반복하게 된다.

    invoke FindClose, FHandle
  .endif
  ret
FillFileInfo endp

모든파일 얻었다면, 검색용 핸들을 닫는다.

그럼 이제 ShowFileInfo함수에 대해서 살펴보자. 인수는 2개로, 아이템 인덱스(행 넘버)와 WIN32_FIND_DATA구조체의 포인터이다.

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
  LOCAL lvi:LV_ITEM
  LOCAL buffer[20]:BYTE
  mov edi, lpFind
  assume edi:ptr WIN32_FIND_DATA

edi 레지스터에WIN32_FIND_DATA구조체의 주소를 설정한다.

mov lvi.imask, LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem, 0

아이템의 레이블과 lParam값을 설정하므로, imask에 LVIF_TEXTLVIF_PARAM 플래그를 지정한다. 다음에, iItem에 행 넘버를, 그리고 이것이 부모 아이템이므로 SubItem에 0 을 설정한다.

lea eax,[edi]. cFileName
mov lvi.pszText, eax
push row
pop lvi.lParam

다음으로, pszText에 레이블명을 설정 한다. 이 경우,WIN32_FIND_DATA구조체의 파일명이 된다. 다음에 정렬을 처리하므로, lParam에 정렬에 필요한 값을 설정한다. 여기서는 행 넘버를 설정한다. 그래서, 인덱스값에 의해 값을 얻을수 있다.

invoke SendMessage, hList, LVM_INSERTITEM, 0, addr lvi

LV_ITEM에 필요한 데이터를 설정 했으므로, 리스트뷰 컨트롤에 LVM_INSERTITEM 메세지를 전송해서, 이 구조체를 삽입한다.

mov lvi.imask, LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf, addr buffer, addr template,[edi]. nFileSizeLow
lea eax, buffer
mov lvi.pszText, eax

2번째의 컬럼에 아이템에 관련된 서브 아이템을 설정한다. 서브 아이템은 레이블명 일뿐이므로, imask에 LVIF_TEXT 를 지정한다. 그리고, 서브 아이템이 존재해야 할 컬럼을 지정한다. 이 때, iSubItem를 증가(increment) 한다. 사용하는 레이블은 파일 사이즈이므로, wsprintf함수를 호출해서 숫자에서 문자열로 변환해야 한다. 그 후, pszText에 그 주소를 설정한다.

  invoke SendMessage, hList, LVM_SETITEM, 0, addr lvi
  assume edi:nothing
  ret
ShowFileInfo endp

LV_ITEM구조체에 대한 설정이 완료되면, 리스트뷰 컨트롤에 설정하기 위해서 LVM_SETITEM 메세지를 전송한다. LVM_INSERTITEM이 아니고 LVM_SETITEM을 사용하는 것에 주의하자. 왜냐하면, 서브 아이템은 아이템의 속성으로서 간주되기 때문이다.

모든 아이템이 리스트뷰 컨트롤에 삽입되면, 배경색과 텍스트를 설정한다.

RGB 255,255,255
invoke SendMessage, hList, LVM_SETTEXTCOLOR, 0, eax
RGB 0,0,0
invoke SendMessage, hList, LVM_SETBKCOLOR, 0, eax
RGB 0,0,0
invoke SendMessage, hList, LVM_SETTEXTBKCOLOR, 0, eax

여기에서는 RGB 매크로를 사용해서, 적, 녹, 청의 각 값을 eax 레지스터에 설정하고 있다. LVM_SETTEXTCOLOR, LVM_SETTEXTDBCOLOR 메세지에 의해서 문자열의 전경, 배경색을 설정한다. 리스트뷰 컨트롤의 배경색은 LVM_SETBKCOLOR 메세지를 전송한다.

invoke GetMenu, hWnd
mov hMenu, eax
invoke CheckMenuRadioItem, hMenu, IDM_ICON, IDM_LIST, IDM_REPORT, MF_CHECKED 

메뉴를 사용해서 사용자가 표시 형식을 선택할 수 있도록 하기 위해서, 메뉴 핸들을 구한다. 사용자가 현재의 표시 형식을 알수 있도록 하기 위해서 메뉴에 라디오 버튼을 표시한다. 현재의 표시 형식을 나타내기 위한 라디오 버튼이므로, CheckMenuRadioItem 함수를 호출해서, 메뉴 아이템보다 먼저 라디오 버튼을 설정 할 필요가 있다.

리스트뷰 컨트롤의 폭과 높이를 0 으로 설정한 것을 주의 하기 바란다. 부모윈도우가 리사이즈 될 때 리스트뷰 컨트롤도 같이 리사이즈 하게 된다. 그래서 부모윈도우와 같은 사이즈로 사이즈를 조정하면 보기에 좋다. 예제에서는 리스트뷰 컨트롤은 부모윈도우의 작업영역 전체로 설정되어 있다.

.elseif uMsg==WM_SIZE
  mov eax, lParam
  mov edx, eax
  and eax, 0ffffh
  shr edx, 16
  invoke MoveWindow, hList, 0, 0, eax, edx, TRUE

부모윈도우가 WM_SIZE 메세지를 받을때, lParam의 하위 워드는 작업영역의 폭, 상위 워드는 높이가 된다. MoveWindow 함수로 부모윈도우의 작업영역 전체를 리스트뷰 컨트롤로 만들어서 리사이즈 한다. 사용자가 메뉴를 선택하면, 리스트뷰 컨트롤이 선택된 표시 형식으로 변경한다. 이것은 SetWindowLong 함수를 사용해서 새로운 스타일로 설정한다.

.elseif uMsg==WM_COMMAND
   .if lParam==0
      invoke GetWindowLong, hList, GWL_STYLE
      and eax, not LVS_TYPEMASK

먼저 현재의 리스트뷰 컨트롤의 표시 형식을 얻은후, 예전의 표시 형식을 제거한다. LVS_TYPEMASK는 4개의 뷰스타일(LVS_ICON LVS_SMALLICON LVS_LIST LVS_REPORT)을 나타내는 정수값이다. 그래서, 현재의 표시 형식 플래그와 "not LVS_TYPEMASK" 값을 and 해서 얻는다. 이것은 결국 현재의 표시 형식을 제거하는 효과가 있다.

메뉴를 생성할 때, 메뉴 ID에 표시 형식 정수를 사용하고 있다.

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

그래서, 부모윈도우가 WM_COMMAND 메세지를 받으면, 메뉴 ID가 되는 wParam의 하위 워드가 표시 형식이 된다.

mov edx, wParam
and edx, 0FFFFh

wParam의 하위 워드만 표시 형식으로 사용되므로, 상위 워드를 0 으로 설정한다.

push edx
or eax, edx

그리고, 선택된 표시 형식을 현재의 스타일에 대입한다.

invoke SetWindowLong, hList, GWL_STYLE, eax 

SetWindowLong 함수에 의해 새로운 스타일을 설정한다.

   pop edx
   invoke CheckMenuRadioItem, hMenu, IDM_ICON, IDM_LIST, edx, MF_CHECKED
. endif

선택된 메뉴 아이템을 설정하기 전에 라디오 버튼을 설정 할 필요가 있다. 그래서, 현재의 표시 형식을 지정해서, CheckMenuRadioItem 함수를 호출한다.

리포트뷰의 컬럼 헤더가 클릭되었을 때, 리스트뷰 컨트롤내의 아이템을 정렬하고 싶기 때문에,WM_NOTIFY메세지에 대해서 처리를 한다.

.elseif uMsg==WM_NOTIFY
   push edi
   mov edi, lParam
   assume edi:ptr NMHDR
   mov eax,[edi]. hwndFrom
   . if eax==hList

WM_NOTIFY 메세지를 받게되면, lParam는 NMHDR구조체의 포인터가 설정되어 있다. 이 메세지가 리스트뷰 컨트롤로부터 전송된 메세지인지는 NMHDR구조체의 hwndFrom멤버가 리스트뷰 컨트롤의 핸들과 같은지를 체크해서 알 수 있다. 만약 일치한다면, 이 메세지는 리스트뷰 컨트롤에서 발생한 것이라고 가정할 수 있다.

.if [edi]. code==LVN_COLUMNCLICK 
   assume edi:ptr NM_LISTVIEW

리스트뷰 컨트롤로부터 통지라면, code 멤버가 LVN_COLUMNCLICK인지 체크해서, 만약 같다면 컬럼 헤더가 클릭된 것이다. LVN_COLUMNCLICK 이라면, lParam가 NMHDR구조체의 슈퍼 세트인 NM_LISTVIEW구조체의 포인터가 된다. 그 후, 사용자가 어느 컬럼 헤더를 클릭했는지를 조사하기 위해서, iSubItem멤버가 몇개인가를 비교한다.

.if [edi]. iSubItem==1 
   . if SizeSortOrder==0  || SizeSortOrder==2

iSubItem이 1 이면, 2번째 컬럼, 즉 파일 사이즈 컬럼이 클릭됨을 알 수 있다. 정렬상태를 나타내는 변수를 사용해서, 0 이면 아직 정렬되지 않은 상태이고, 1 이면 오름차순 정렬이 된상태며, 2 라면 내림차순 정렬이 끝난 상태라고 나타내기로 한다. 아이템/서브 아이템이 내림차순으로 정렬되어 있으면, 오름차순으로 정렬을 다시 하기로 한다.

invoke SendMessage, hList, LVM_SORTITEMS, 1, addr CompareFunc

wParam를 1 로 해서, lParam에 비교 함수의 포인터를 지정하고, LVM_SORTITEMS 메세지를 리스트뷰 컨트롤에 전송한다. wParam의 값은 사용자 정의값이므로, 어떠한 값이라도 지정할 수 있다. 예제에서는 정렬방식을 나타내기로 하고 있다. 그렇다면 비교 함수를 살펴보자.

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
  LOCAL buffer[256]:BYTE
  LOCAL buffer1[256]:BYTE
  LOCAL lvi:LV_ITEM

  mov lvi.imask, LVIF_TEXT
  lea eax, buffer
  mov lvi.pszText, eax
  mov lvi.cchTextMax, 256

비교 함수는, 리스트뷰 컨트롤이 호출하게 되지만, 그 때 LV_ITEM 구조체의 lParam의 값을 lParam1 과 lParam2에 설정하게 된다. lParam에는 아이템의 인덱스값을 설정했다는것을 기억하기 바란다. 그래서, 인덱스값을 사용해서 리스트뷰 컨트롤에 질의를 하게되면 아이템에 관한 정보를 얻을 수 있다. 얻은정보는 아이템/서브 아이템의 레이블명이므로, LV_ITEM구조체의 imask멤버를 LVIF_TEXT로 해서, pszText cchTextMax를 버퍼의 주소, 버퍼의 사이즈로 설정한다.

.if SortType==1
   mov lvi.iSubItem, 1
   invoke SendMessage, hList, LVM_GETITEMTEXT, lParam1, addr lvi 

SortType의 값이 1 이나 2 라면 컬럼 사이즈가 클릭된 것이 된다. 1은 오름차순으로 정렬한 것을 나타내며 , 2라면 그 내림차순으로 정렬한 것을 의미하므로,iSubItem을 1 로(파일 사이즈의 컬럼 넘버) 리스트뷰 컨트롤에 LVM_GETITEMTEXT 메세지를 전송해서, 서브 아이템의 레이블명(여기에서는 파일 사이즈)을 얻는다.

invoke String2Dword, addr buffer
mov edi, eax

이번에는 직접 생성한 String2Dword 함수로, 사이즈 문자열을 dword값으로 변환한다. dword값이 eax 레지스터에 반환되므로, 다음에 사용하기 위해 edi 레지스터에 설정한다.

invoke SendMessage, hList, LVM_GETITEMTEXT, lParam2, addr lvi
invoke String2Dword, addr buffer
sub edi, eax
mov eax, edi

lParam2도 다음과 같이 해서, 2개의 파일 사이즈값을 구한다.

  • eax가 거짓값이면 1번째의 아이템이 앞에 온다
  • eax가 참값을 가지면 2번째의 아이템이 앞에 온다
  • eax가 0 이면 두개가 같다

이 경우, 오름차순으로 아이템을 정렬하고 싶기 때문에, 단순히 첫번째 값으로부터 2번째의 값을 빼서, 결과를 eax 레지스터에 저장하면 된다.

.elseif SortType==3
   mov lvi.iSubItem, 0
   invoke SendMessage, hList, LVM_GETITEMTEXT, lParam1, addr lvi 
   invoke lstrcpy, addr buffer1, addr buffer
   invoke SendMessage, hList, LVM_GETITEMTEXT, lParam2, addr lvi 
   invoke lstrcmpi, addr buffer1, addr buffer

사용자가 파일명 컬럼을 클릭했을 때, 파일명을 비교하지만, 먼저 파일명을 얻은 후 lstrcmpi 함수로 비교작업을 수행한다. lstrcmpi 함수의 반환값이 음수라면 1 번째의 인수가 2 번째의 인수보다 작다는 의미이다.

아이템을 정렬 할 때, UpdatelParam 함수를 호출해서 새로운 인덱스값 lParam에 적용시킬 필요가 있다.

invoke UpdatelParam
mov SizeSortOrder, 1

이 함수는 단지 리스트뷰 컨트롤내의 모든아이템을 구하고, lParam을 새로운 인덱스값으로 갱신한다. 이 작업을 하지 않으면, lParam이 아이템의 인덱스값이라 가정하고 있으므로 정렬이 원활히 진행되지 않게 된다.

.elseif [edi]. code==NM_DBLCLK 
   invoke ShowCurrentFocus
.endif

사용자가 아이템을 더블 클릭 하면, 아이템의 레이블명을 메시지 박스로 출력하기 때문에, NMHDR의 code 멤버가 NM_DBLCLK 인지를 체크해야 한다. 더블 클릭이면, 그 아이템의 레이블명을 얻어서, 메시지 박스로 출력한다.

ShowCurrentFocus proc
   LOCAL lvi:LV_ITEM
   LOCAL buffer[256]:BYTE
   invoke SendMessage, hList, LVM_GETNEXTITEM,-1, LVNI_FOCUSED

어떤 아이템이 더블 클릭 된지는 어떻게 하면 알수 있을까. 아이템이 클릭이나 더블 클릭 되었을 때, 그 아이템은"포커스"상태가 된다. 복수의 아이템이 하이라이트 되더라도(선택되고 있었다 해도), 포커스 상태는 한개만 존재한다. 그래서 포커스 상태가 되어 있는 아이템을 찾아내면 되는 것이다.
그 후, 리스트뷰 컨트롤에 LVM_GETNEXTITEM 메세지를 전송한다. 이 때, lParam에는 검색하고 싶은 상태를 지정하고, wParam는 모든아이템을 검색하라는 의미로 -1 로 설정 한다. eax 레지스터에 발견된 아이템 인덱스값이 반환되게된다.

mov lvi.iItem, eax
mov lvi.iSubItem, 0
mov lvi.imask, LVIF_TEXT

lea eax, buffer mov lvi.pszText, eax
mov lvi.cchTextMax, 256
invoke SendMessage, hList, LVM_GETITEM, 0, addr lvi

다음으로, 리스트뷰 컨트롤에 LVM_GETITEM 메세지를 전송해서, 레이블명을 얻는다.

invoke MessageBox, 0, addr buffer, addr AppName, MB_OK 

이제는 메시지 박스에 레이블명을 표시할 수 있게 된다.

리스트뷰 컨트롤에 아이콘을 사용하고 싶다면, 트리뷰컨트롤을 다룬 장을 살펴보기 바란다. 하지만 대부분의 순서는 거의 같다.



Posted by openserver