第 24 章 通用控制項(4):樹狀檢視控制項 ( Tree View )


樹狀檢視控制項的外觀

樹狀檢視控制項也是一種視窗,而該視窗內所包含的項目是具有階層的。參考底下的對話盒,HDINFO2,它是本章範例,此對話盒是由左邊的「樹狀檢視控制項」與右邊的「靜態控制項」組成。「樹狀檢視控制項」內有許多項目 ( item ),這些項目可以擁有下一層的項目,這種下一層的項目稱為「子項目」( child item ),子項目是附屬於「父項目」( parent item ) 之下。最頂層的項目,是沒有「父項目」的,稱為「根項目」( root item )。像這種,某個項目附屬於另一個項目,稱為階層性,如同樹幹依附樹根,樹枝依附樹幹。

上圖中,「電腦中的硬碟」就是所謂的根項目,其下有三個子項目:「實體磁碟 0」、「實體磁碟 1」、「實體磁碟 2」,這些「實體磁碟 0」、「實體磁碟 1」等稱為項目名稱或標籤 ( item label )。這三個子項目之下又各有子項目,對「電腦中的硬碟」而言,就是「孫項目」了,例如「實體磁碟 2」就有一個子項目,稱為「D:」,是「電腦中的硬碟」的孫項目。在預設的情形下,樹狀檢視的項目前是沒有按鈕與線條,無法清楚的顯示階層關係,也無法得知某項目是否含子項目,須以滑鼠雙擊,如果能顯示下一層的子項目才能知道其實該項目含有子項目。

但是,假如樹狀檢視設有 TVS_HASBUTTONS 風格,則會在每個具有子項目的項目前出現按鈕,分別表示未展開 ( 各子項目收攏起來 ) 或已展開 ( 把子項目顯示於該項目底下 ) 狀態;使用者也可以以滑鼠單擊這兩種按鈕,以展開或收攏項目。但是根項目前不會有按鈕,如果要於根項目前顯示按鈕,須加上 TVS_LINESATROOT 風格。如果該項目沒有子項目,就不顯示按鈕。假如設定了 TVS_HASLINES 風格,系統還會以虛線連接父項目與子項目。這樣就變成上圖的視窗了。

在預設的情形下,如果滑鼠游標在某個項目上空,沒按滑鼠左鍵,而該項目名稱又超出視窗範圍,那麼樹狀檢視會彈出一個工具提示 ( tooltip ) 顯示完整的項目名稱。這個工具提示也叫 infotip,它是樹狀檢視的子視窗,程式可以設定 TVS_NOTOOLTIPS 風格,禁止顯示工具提示。也可以處理 TVN_GETINFOTIP 通知碼,設定工具提示的風格或文字。上圖中,以淡黃色底的氣球狀的視窗,就是工具提示。


樹狀檢視控制項概說

建立樹狀檢視控制項

建立樹狀檢視控制項有兩種方法:一是在資源描述檔中的對話盒面板堜w義;或者是在程式中呼叫 CreateWindowEx 建立。不論是哪一種方法,所使用的視窗類別都是「SysTreeView32」,並且要呼叫 InitCommonControls 或 InitCommonControlsEx。如底下是呼叫 CreateWindowEx 建立樹狀檢視控制項:

.CONST
szClass DB  "SysTreeView32",0

.CODE
INVOKE  CreateWindowEx,0,OFFSET szClass,0,WS_BORDER or WS_CHILD or WS_VISIBLE,\
        10,10,160,200,hWnd,0,hInstance,0
mov     hTreeView,eax

樹狀檢視的風格 ( style )

底下是樹狀檢視的風格:

風格說明
TVS_CHECKBOXES 4.70 版及其以後的 COMCTL32.DLL 才可使用,在項目前顯示檢驗盒,可供使用者勾選,而且只有樹狀檢視與 Image List 連結後才會顯示檢驗盒。更詳細的細節參考 MSDN。
TVS_DISABLEDRAGDROP不發送 TVN_BEGINDRAG 給樹狀檢視控制項,因此無法以滑鼠拖曳項目。
TVS_EDITLABELS允許就地編輯項目名稱
TVS_FULLROWSELECT只有 4.71 版及其以後的 COMCTL32.DLL 才可使用,使用者選取項目時,整列都會變高亮度顯示,不能與 TVS_HASLINES 搭配使用
TVS_HASBUTTONS顯示按鈕,使用者按此按鈕能展開或收攏
TVS_HASLINES以虛線段把父項目與子項目連接,表示階層關係
TVS_INFOTIP4.70 版及其以後的 COMCTL32.DLL 才可使用,具有此風格時,當滑鼠移過項目上空時,樹狀檢視會發送 TVN_GETINFOTIP 通知碼給父視窗。
TVS_LINESATROOT以虛線連接根項目。如果沒指定 TVS_HASLINES,系統會忽略此風格。
TVS_NOHSCROLL5.80 版及其以後的 COMCTL32.DLL 才可使用,此風格使樹狀檢視不顯示水平捲軸
TVS_NONEVENHEIGHT項目預設高度為偶數值,設定此風格後可以發出 TVM_SETITEMHEIGHT 設定奇數值的項目高度,4.71 版及其以後的 COMCTL32.DLL 才可使用。
TVS_NOSCROLL4.71 版及其以後的 COMCTL32.DLL 才可使用,不顯示水平捲軸及垂直捲軸
TVS_NOTOOLTIPS4.70 版及其以後的 COMCTL32.DLL 才可使用,禁用工具提示 ( 在樹狀檢視控件建立的同時,也會建立一個工具提示為其子視窗,如果不需要建立工具提示,就要指定 TVS_NOTOOLTIPS。樹狀檢視的工具提示好像也叫 INFOTIP。)
TVS_RTLREADING4.70 版及其以後的 COMCTL32.DLL 才可使用,項目名稱由右至左顯示,只有在希伯來語、阿拉伯語等版本的 Windows 才有可用此風格。
TVS_SHOWSELALWAYS即使樹狀檢視失去輸入焦點,也能以高亮度顯示被選定的項目
TVS_SINGLEEXPAND4.71 版及其以後的 COMCTL32.DLL 才可使用,只有被選取項目底下的子項目展開,其餘項目都會收攏;而且滑鼠點擊某項目一次就能展開子項目,再點擊該項目就收攏。如果在以滑鼠點選項目同時,還按住 Ctrl 鍵,則其餘項目不會收攏或展開。
TVS_TRACKSELECT4.71 版及其以後的 COMCTL32.DLL 才可使用,滑鼠游標指到任一項目時,該項目會變成「超連結」的樣子,滑鼠游標變成手形。

即使樹狀檢視已建立好了,我們還是可以事後改變或存取樹狀檢視的風格,前者可以呼叫 SetWindowLong;後者則是呼叫 GetWindowLong。請參考第 22 章

樹狀檢視的延伸風格 ( extended style )

底下是 Tree View 的延伸風格:

延伸風格說明
TVS_EX_AUTOHSCROLL不顯示水平捲軸,但是可以把所選取的項目,自動的顯示在樹狀檢視可看見的區域中。
TVS_EX_DIMMEDCHECKBOXES
TVS_EX_DOUBLEBUFFERTVS_EX_DOUBLEBUFFER 會告訴樹狀檢視要透過雙重緩衝進行繪製,這可以避免控制項在調整大小時閃爍。
TVS_EX_DRAWIMAGEASYNC
TVS_EX_EXCLUSIONCHECKBOXES
TVS_EX_FADEINOUTEXPANDOS可要求淡出效果
TVS_EX_MULTISELECT在 Windows XP 之前都不支援,最好別使用
TVS_EX_NOINDENTSTATE
TVS_EX_PARTIALCHECKBOXES
TVS_EX_RICHTOOLTIP

樹狀檢視控制項的延伸風格似乎跟清單檢視一樣,都不能在資源檔堻]定,因此只好對樹狀檢視發出 TVM_SETEXTENDEDSTYLE 訊息,來設定延伸風格。不過為了不破壞其他延伸風格,僅僅設定我們想要的延伸風格,所以應該先以 TVM_GETEXTENDEDSTYLE 取得延伸風格,再做運算,最後才把延伸風格傳給樹狀檢視堙C例如要設定 TVS_EX_AUTOHSCROLL,作法應該如下程式:

        INVOKE  SendMessage,hTreeView,TVM_GETEXTENDEDSTYLE,0,0              ;取得延伸風格,存於 EAX
        or      eax,TVS_EX_AUTOHSCROLL                                      ;加上 TVS_EX_AUTOHSCROLL
        INVOKE  SendMessage,hTreeView,TVM_SETEXTENDEDSTYLE,hTreeView,eax    ;發送延伸風格給樹狀檢視

如果要取消 TVS_EX_AUTOHSCROLL,做法如下:

        INVOKE  SendMessage,hTreeView,TVM_GETEXTENDEDSTYLE,0,0              ;取得延伸風格,存於 EAX
        or      eax,not TVS_EX_AUTOHSCROLL                                  ;取消 TVS_EX_AUTOHSCROLL
        INVOKE  SendMessage,hTreeView,TVM_SETEXTENDEDSTYLE,hTreeView,eax    ;發送延伸風格給樹狀檢視

樹狀檢視的項目狀態 ( item state )

樹狀檢視堛漕C個項目都有某些狀態,這些狀態是指是否被選取的 ( selected )、是否有效的 ( disabled )、是否展開的 ( expanded )……等等。每一種狀態都由一個位元表示,大部分的情形下,系統會自行設定,例如當使用者以滑鼠在項目上點擊時,就會使該項目自動的變為被選取的;但是程式也能自行設定某項目的狀態,要這樣做可以對樹狀檢視發出 TVM_SETITEM 訊息設定某個項目的狀態,程式也可以發送 TVM_GETITEM 訊息以取得某個項目的狀態。底下是發出 TVM_SETITEM 訊息的過程:

        INVOKE  SendMessage,hTreeView,TVM_SETITEM,0,ADDR tvi

hTreeView 是樹狀檢視的代碼,wParam 不使用須設為 0,lParam 是結構體 tvi 的位址,tvi 是一個稱為 TVITEM 的結構體,TVITEM 結構體的各個欄位是:

TVITEM          STRUCT
imask           UINT    ?
hItem           HANDLE  ?
state           UINT    ?
stateMask       UINT    ?
pszText         LPTSTR  ?
cchTextMax      DWORD   ?
iImage          DWORD   ?
iSelectedImage  DWORD   ?
cChildren       DWORD   ?
lParam          LPARAM  ?
TVITEM          ENDS

TVITEM 的第一個欄位是 imask,在 MSDN 稱為 mask,但 mask 與 MASM 的保留字相同,故 MASM32 改為 imask 或 _mask,此欄位指出 TVITEM 堛滬些欄位是有效的,必須設定,下表列出 imask 與其他欄位的關係:

imask 影響欄位說   明
TVIF_HANDLE hItem 項目的代碼
TVIF_STATE state
stateMask
設定或取得項目的狀態
TVIF_TEXT pszText
cchTextMax
pszText 指向以 NULL 結尾的字串,做為項目的名稱;如果 pszText 為 LPSTR_TEXTCALLBACK 的話,那麼樹狀檢視控制項會在要顯示、或排序、或編輯項目名稱時,或是項目名稱改變而發出 TVN_GETDISPINFO 通知碼給父視窗,程式應在處理此通知碼時設好項目名稱。
cchTextMax 是 pszText 所指字串的大小,以字元為單位,如果是設定字串,因為可由 NULL 做結尾,所以 cchTextMax 會被系統忽略。
TVIF_IMAGE iImage image list 的索引值,可顯示出項目未被選定時的圖示。如果此欄位為 I_IMAGECALLBACK,則樹狀檢視會把在需要顯示圖示時,發出 TVN_GETDISPINFO 通知碼給父視窗,父視窗的視窗函式處理此通知碼時再指定未被選定的圖示。
TVIF_SELECTEDIMAGE iSelectedImage image list 的索引值,可顯示出項目被選定時的圖示。如果此欄位為 I_IMAGECALLBACK,則樹狀檢視會把在需要顯示圖示時,發出 TVN_GETDISPINFO 通知碼給父視窗,父視窗的視窗函式處理此通知碼時再指定被選定的圖示。
TVIF_CHILDREN cChildren 指出此項目底下有多少個子項目,可以是:
  1. 0:沒有子項目,如果樹狀檢視具有 TVS_HASBUTTONS,項目前會顯示
  2. 1:預定有一個或一個以上的子項目,如果具有 TVS_HASBUTTONS 的樹狀檢視,剛加入的項目可能沒有子項目,但如果 cChildren 設為 1,則該項目前仍會顯示
  3. I_CHILDRENCALLBACK:樹狀檢視在需要顯示項目時,傳送 TVN_GETDISPINFO 訊息給父視窗,用來指定此項目的子項目。如果樹狀檢視具有 TVS_HASBUTTONS 風格,可以強迫顯示有子項目的按鈕,不論是否真有子項目。
TVIF_PARAM lParam 程式自行定義的數值

當程式要自行改變某個項目狀態時,通常僅僅改變一個狀態,而其餘狀態不變,亦即其餘狀態的位元須遮罩 ( mask )。例如底下的程式是使某個項目變粗體字,而其餘狀態則不變:

        mov     tvi.imask,TVIF_HANDLE or TVIF_STATE
        mov     ecx,hitem
        mov     tvi.hItem,ecx                       ;改變 hitem 項目為粗體字狀態
        mov     tvi.state,TVIS_BOLD                 ;粗體字狀態
        mov     tvi.stateMask,TVIS_BOLD             ;遮罩除了粗體字以外的位元
        INVOKE  SendMessage,hTreeView,TVM_SETITEM,0,OFFSET tvi

上面程式的第一行,就是告訴系統 TVITEM 欄位只有 hItem、state 有效,第四行則是改變狀態為粗體,第五行則是只有粗體那個位元變動,其餘不變。

底下是樹狀檢視的項目狀態:

狀態十六進位
數值
說明
TVIS_FOCUSED1 此項目處於焦點狀態,也就是被虛線框包圍住。
TVIS_SELECTED2 此項目處於被選取的狀態,此時該項目的底色會以反白顯示,如果同時也具有輸入焦點 ( 大部份的情形下 ),那麼該項目也會為虛線外框包圍住。
TVIS_CUT4 此項目處於標記 ( marked ) 狀態。
TVIS_DROPHILITED8 此項目被選為拖曳 ( drag and drop ) 的目標,亦即當使用者拖曳甲項目到乙項目時,乙項目具有 TVIS_DROPHILITED 狀態。
TVIS_BOLD10h 項目為粗體字。
TVIS_EXPANDED20h 某個父項目底下的子項目已被展開,其子項目能顯示出來,只有具有子項目的項目才能有此狀態。
TVIS_EXPANDEDONCE40h 某個父項目底下的子項目曾經被展開過,此狀態只能設在具有子項目的父項目上。如果在處理 TVM_EXPAND 訊息時,父項目有此狀態,就不會為產生 TVN_ITEMEXPANDING 和 TVN_ITEMEXPANDED 通知碼。若想除去此狀態,可以發出 TVM_EXPAND 訊息,並使 wParam 為「TVE_COLLAPSE or TVE_COLLAPSERESET」。
TVIS_OVERLAYMASK0F00h 用來獲得項目重疊圖片索引的遮罩碼
TVIS_STATEIMAGEMASK0F000h 用來獲得項目狀態圖片索引的遮罩碼
TVIS_USERMASK0F000h 與 TVIS_STATEIMAGEMASK 相同

事實上,在 TVITEM 結構體堛 state 欄位,只有 0∼7 位元,才是表示項目狀態;8∼11 位元,表示重疊圖片 ( overlay image ),這四個位元表示一個十六位元數值,此數值為 image list 的索引,由一開始。如果此數值為零,表示沒有重疊圖片。要取得重疊圖片索引,就必須使 state 欄位與 TVIS_OVERLAYMASK 做 AND 運算。state 欄位的 12∼15 位元,表示狀態圖片,由一開始,如果此數值為零,表示沒有狀態圖片。要取得狀態圖片索引,就必須使 state 欄位與 TVIS_STATEIMAGEMASK 做 AND 運算。


在樹狀檢視中的項目 ( item )

在樹狀檢視中新增、查詢項目等等動作,都是呼叫 SendMessage,對樹狀檢視發出特定的訊息,該訊息包含操作及所需資料,而呼叫成功後,樹狀檢視即依該操作完成動作。例如要新增項目就是發出 TVM_INSERTITEM 訊息。當使用者對樹狀檢視中的項目做修改或拖曳 ( drag and drop ) 時,樹狀檢視則是發出通知碼給父視窗,而父視窗則在處理該通知碼時,做一些處理。

增添項目

要在樹狀檢視控制項中新增一個項目,必須對樹狀檢視控制項發出 TVM_INSERTITEM 訊息,程式如下:

        INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis

如果成功插入新項目,傳回新項目的代碼,否則返回 0。這個訊息的傳回值與清單檢視控制項 ( list view ) 插入項目訊息 ( LVM_INSERTITEM ) 的傳回值很不同, LVM_INSERTITEM 訊息傳回索引值,但是 TVM_INSERTITEM 卻傳回代碼,主要是因為樹狀檢視控制項的子項目和父項目有階層的關係,所以為了釐清各項目之間的關係,每個項目都有代碼。上面程式堙AhTreeView 是樹狀檢視控制項代碼,此代碼可接收到 TVM_INSERTITEM 訊息,以增添新項目。tvis 是一個稱為 TVINSERTSTRUCT 結構體 ( 在 MASM32 v.11 的 WINDOWS.INC v.1.60 也稱為 TV_INSERTSTRUCT 結構體 ),SendMessage 的最後一個參數是 TVINSERTSTRUCT 結構體的位址,此結構體內含有新加入項目的資料,其欄位是:

TVINSERTSTRUCT  STRUC
hParent         HANDLE      ?
hInsertAfter    HANDLE      ?
UNION
  itemex        TVITEMEX    <>
  item          TVITEM      <>
ENDS
TVINSERTSTRUCT  ENDS

第一個欄位,hParent,是父項目的代碼,如果是要插入根項目,此欄位為 TVI_ROOT 或 NULL。hInsertAfter 是某個項目的代碼,新加入的項目會安插在此項目之後,但也可以是 TVI_FIRST,表示插在同一階層項目的第一個;或是 TVI_LAST,表示插在最後一個;或是 TVI_SORT,表示依項目名稱排序。第三個欄位是 TVITEMEX 結構體或 TVITEM 結構體。4.71 版及其以後的 COMCTL32.DLL 可用 TVITEMEX,否則只能使用 TVITEM。安裝了 IE 4.0 以後,COMCTL32.DLL 就是 4.71 版了。TVITEM 也叫 TV_ITEM,前面已經說明過了。TVITEMEX 也叫 TV_ITEMEX,是 TVITEM 的延伸,TVITEMEX 結構體的欄位如下:

TVITEMEX        STRUCT
imask           UINT    ?
hItem           HANDLE  ?
state           UINT    ?
stateMask       UINT    ?
pszText         LPTSTR  ?
cchTextMax      DWORD   ?
iImage          DWORD   ?
iSelectedImage  DWORD   ?
cChildren       DWORD   ?
lParam          LPARAM  ?
iIntegral       DWORD   ?
uStateEx        DWORD   ?
hwnd            HWND    ?
iExpandedImage  DWORD   ?
TVITEMEX        ENDS

從 imask 到 lParam 是屬於 TVITEM 結構體,iIntegral 到 iExpandedImage 是 TVITEMEX 多出的部份,而 uStateEx 到 iExpandedImage 則是 Windows XP 及其以後的系統才有的欄位。底下的表格是 imask 與各欄位之間的關係:

imask 影響欄位說   明
TVIF_HANDLE hItem 項目的代碼
TVIF_STATE state
stateMask
設定或取得項目的狀態,請參考樹狀檢視的項目狀態
TVIF_TEXT pszText
cchTextMax
pszText 指向以 NULL 結尾的字串,做為項目的名稱;如果 pszText 為 LPSTR_TEXTCALLBACK 的話,那麼樹狀檢視控制項會在要顯示、或排序、或編輯項目名稱時,或是項目名稱改變而發出 TVN_GETDISPINFO 通知碼給父視窗,程式應在處理此通知碼時設好項目名稱。
cchTextMax 是 pszText 所指字串的大小,以字元為單位,如果是設定字串,因為可由 NULL 做結尾,所以 cchTextMax 會被系統忽略。
TVIF_IMAGE iImage image list 的索引值,可顯示出項目未被選定時的圖示。如果此欄位為 I_IMAGECALLBACK,則樹狀檢視會把在需要顯示圖示時,發出 TVN_GETDISPINFO 通知碼給父視窗,父視窗的視窗函式處理此通知碼時再指定未被選定的圖示。
TVIF_SELECTEDIMAGE iSelectedImage image list 的索引值,可顯示出項目被選定時的圖示。如果此欄位為 I_IMAGECALLBACK,則樹狀檢視會把在需要顯示圖示時,發出 TVN_GETDISPINFO 通知碼給父視窗,父視窗的視窗函式處理此通知碼時再指定被選定的圖示。
TVIF_CHILDREN cChildren 指出此項目底下有多少個子項目,可以是:
  1. 0:沒有子項目
  2. 1:有一個或一個以上的子項目
  3. I_CHILDRENCALLBACK:樹狀檢視在需要顯示項目時,傳送 TVN_GETDISPINFO 訊息給父視窗,用來指定此項目的子項目。如果樹狀檢視具有 TVS_HASBUTTONS 風格,可以強迫顯示有子項目的按鈕,不論是否真有子項目。
TVIF_PARAM lParam 程式自行定義的數值
TVIF_INTEGRALiIntegral 項目的高度。如果此欄位為 2,表示此項目高度為正常的 2 倍;如果此欄位為 3,表示此項目高度為正常的 3 倍。此欄位只設定單一項目的高度,要設定所有項目的高度,要發送 TVM_SETITEMHEIGHT 訊息給樹狀檢視。
TVIF_STATEEXuStateEx 只有 XP 及其以後的系統可用此旗標。可以是下面其中一種:
  1. TVIS_EX_DISABLED:以灰色表示的狀態,使用者無法選取。
  2. TVIS_EX_FLAT:看不見的項目
  3. TVIS_EX_HWND:
 hwnd 未使用,應設為 0。
TVIF_EXPANDEDIMAGEiExpandedImage Vista 及其以後系統才可使用,展開的子項目圖示,此圖示是 image list 中的索引。
TVIF_DI_SETITEM   只能在處理 TVN_GETDISPINFO 時可用,它可維持原值。

在項目前加入圖片

如果想再項目左邊加上圖片的話,有四個步驟:

  1. 首先必須呼叫 ImageList_Create 建立「圖片清單」( image list ),ImageList_Create 原型是:

            ImageList_Create   PROTO   cx:DWORD,cy:DWORD,flags:DWORD,cInitial:DWORD,cGrow:DWORD

    如果成功建立,則會傳回圖片清單的代碼,詳細說明參考第 21 章

  2. 將需要用到的圖片,如 BMP 位元圖或圖示,加入到圖片清單堙C如果是要加入位元圖,可以呼叫 ImageList_Add;要加入圖示,呼叫 ImageList_ReplaceIcon。詳細說明參考第 21 章

  3. 發送 TVM_SETIMAGELIST 給樹狀檢視,把圖片清單與樹狀檢視連結起來。方法是:

            INVOKE  SendMessage,hTreeView,TVM_SETIMAGELIST,iImage,himl

    iImage 參數可以有兩種值:TVSIL_NORMAL 和 TVSIL_STATE,前者代表被選定或未被選定的圖片;後者代表程式設計師自行定義的的狀態圖片。himl 則是圖片清單的代碼,若 himl 為 NULL,則系統會移除樹狀檢視的 image list。

  4. 最後一步,則是在新添加項目時 ( 發出 TVM_INSERTITEM 訊息給樹狀檢視 ) 或者對在已存在的項目發出 TVM_SETITEM 時,設定 TVITEM 結構體的 imask、iImage、iSelectedImage 欄位。要設定正常顯示或未被選取的圖片時,必須設定 imask 的 TVIF_IMAGE 旗標,並於 iImage 欄位填上 image list 的第幾個圖片。要顯示被選取的圖片,則要設定 imask 的 TVIF_SELECTEDIMAGE 旗標,並於 iSelectedImage 欄位填上 image list 的第幾個圖片。

取得項目的資料

前面提過,在樹狀檢視堥C新添一個項目,該項目就擁有一個代碼,那麼,程式需不需要畫出一塊記憶體來保存每個項目的代碼呢?答案是不需要,尤其是把磁碟堛漸媬當成樹狀檢視的項目時,這時項目的數量可能會無法預知而且可能非常多。既然不必保存項目的代碼,那麼該如何才能取得項目的資料呢?一般而言,要取得某個項目的資料有兩步驟:

底下以取得根項目的名稱為例子,說明這兩個步驟,程式碼如下:

        LOCAL   tvi:TVITEM
        LOCAL   buffer[200]:BYTE                                    ;存放根項目名稱處
        INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_ROOT,0   ;EAX=根項目代碼
        lea     edx,buffer
        mov     tvi.hItem,eax
        mov     tvi.imask,TVIF_TEXT
        mov     tvi.pszText,edx
        mov     tvi.cchTextMax,SIZEOF buffer
        INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvi        ;呼叫成功後,buffer 存有根項目名稱

如果要得到根項目有幾個子項目,則使 tvi.imask 欄位變成「TVIF_TEXT or TVIF_CHILDREN」,呼叫成功後,會在 cChildren 欄位存有子項目個數。

就地編輯項目名稱

使用 TVN_BEGINLABELEDIT 與 TVN_ENDLABELEDIT

假如樹狀檢視具有「TVS_EDITLABELS」風格時,就可以使用就地編輯。就地編輯的意思是,在某個被選定的項目上,再以滑鼠左鍵單擊一次,就會在該項目上出現一個編輯框,可以供使用者修改項目名稱。在程式中要處理就地編輯,必須處理兩個通知碼,TVN_BEGINLABELEDIT 和 TVN_ENDLABELEDIT。這兩個通知碼是使用者做了就地編輯的動作之後,透過樹狀檢視傳給父視窗的視窗函式,父視窗再加以處理。父視窗也可以自行發出 TVM_EDITLABEL 訊息給樹狀檢視,要求就地編輯。

當使用者做了就地編輯的動作或父視窗對樹狀檢視發出 TVM_EDITLABEL 之後,樹狀檢視會建立一個編輯框供使用者編輯項目名稱。在此編輯框尚未顯示於螢幕時,樹狀檢視便發出 TVN_BEGINLABELEDIT 通知碼給父視窗,程式可以在處理 TVN_BEGINLABELEDIT 通知碼時,發出 TVM_GETEDITCONTROL 訊息給樹狀檢視而獲得編輯框代碼,使編輯框符合要求。例如發出 EM_SETLIMITTEXT 給編輯框限制輸入字數。

待使用者以滑鼠左鍵單擊其他區域,表示使用者輸入完成,這時樹狀檢視發出 TVN_ENDLABELEDIT 通知碼給父視窗。這段時期內,父視窗應由把編輯框的內容設定給項目名稱。方法有幾個,其中一是由編輯框獲得其內容,存於某位址內,再把該位址指定給 TVITEM 結構體中的 pszText 欄位,最後發出 TVM_SETITEM 訊息給樹狀檢視,設定新的項目名稱。待父視窗設定好項目的新名稱之後,樹狀檢視再將編輯框銷毀。整個過程如下︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
hTreeView       DWORD   ?
hEdit           DWORD   ?
 
        LOCAL   tviex:TVITEMEX          ;tviex 結構體為 26 行發出 TVM_SETITEM 訊息所用
        LOCAL   buffer[40]:BYTE
 
.ELSEIF uMsg==WM_NOTIFY
                push    ebx
                ASSUME  ebx:PTR NM_TREEVIEW
                mov     ebx,lParam
                mov     eax,[ebx].hdr.hwndFrom                          ;EAX=發出 WM_NOTIFY 的控制項代碼
    .IF eax==hTreeView
        .IF [ebx].hdr.code==TVN_BEGINLABELEDIT                          ;通知碼是否為 TVN_BEGINLABELEDIT
                ASSUME  ebx:PTR NMTVDISPINFO
                INVOKE  SendMessage,hTreeView,TVM_GETEDITCONTROL,0,0    ;取得編輯框代碼
                mov     hEdit,eax
                INVOKE  SendMessage,eax,EM_SETLIMITTEXT,32,0            ;設定最多僅能輸入 32 個字
        .ELSEIF [ebx].hdr.code==TVN_ENDLABELEDIT                        ;通知碼是否為 TVN_ENDLABELEDIT
                ASSUME  ebx:PTR NMTVDISPINFO
                INVOKE  GetWindowText,hEdit,ADDR buffer,32              ;取得編輯框內容,存於 buffer 區域變數
                mov     edx,[ebx].item.hItem                            ;取得項目代碼
                mov     tviex.hItem,edx                                 ;把項目代碼存入 tviex 結構體內
                mov     tviex.imask,TVIF_TEXT                           ;設定旗標,僅改變 pszText
                lea     ecx,buffer
                mov     tviex.pszText,ecx                               ;把 pszText 指向 buffer 位址
                INVOKE  SendMessage,hTreeView,TVM_SETITEM,0,ADDR tviex  ;設定項目
        .ENDIF
    .ENDIF
                ASSUME  ebx:PTR NOTHING
                pop     ebx

剛才提過,當使用者在樹狀檢視的某個被選定的項目上,再以滑鼠左鍵單擊一次後,樹狀檢視便會發出 WM_NOTIFY 給父視窗。WM_NOTIFY 的 wParam 是樹狀檢視的識別碼,而 lParam 則是 NMTVDISPINFO 結構體位址,此時這個結構體的 hdr.hwndFrom 為樹狀檢視的代碼,hdr.idFrom 為樹狀檢視的識別碼,hdr.code 為 TVN_BEGINLABELEDIT。而 NMTVDISPINFO 結構體的 item 欄位中,hItem、state、lParam 及 pszText 的資料會被系統填好,可供父視窗使用。

當使用者結束就地編輯時,樹狀檢視還是發出 WM_NOTIFY 訊息給父視窗,其中 hdr 的欄位跟上面一樣,只有 hdr.code 變為 TVN_ENDLABELEDIT。而 item 欄位的 hItem、lParam 及 pszText 的資料會被填好,可供父視窗使用。也就是說,當父視窗收到 TVN_ENDLABELEDIT 通知碼時,項目名稱已經被填在 pszText 所指位址內,hItem 內也已存有項目代碼。所以處理 TVN_ENDLABELEDIT 其實可以簡化如下︰

1
2
3
4
5
6
        .ELSEIF [ebx].hdr.code==TVN_ENDLABELEDIT                        ;通知碼是否為 TVN_ENDLABELEDIT
                ASSUME  ebx:PTR NMTVDISPINFO
                mov     [ebx].item.imask,TVIF_TEXT                      ;更改 imask 旗標,以配合 TVM_SETITEM 設定項目名稱
                lea     eax,[ebx].item                                  ;EAX 指向 ITEM 結構體位址
                INVOKE  SendMessage,hTreeView,TVM_SETITEM,0,eax         ;設定項目
        .ENDIF

如果沒有發出 TVM_SETITEM,重新設定項目,或者以其他方法更改項目名稱,那麼項目名稱仍然會是原來的。不過上述方法,雖經簡化,但仍嫌麻煩,請看底下的「TVN_BEGINLABELEDIT 與 TVN_ENDLABELEDIT 的返回值」。

TVN_BEGINLABELEDIT 與 TVN_ENDLABELEDIT 的返回值

說起來,對話框的返回值蠻複雜的,必須要有正確的觀念才行。對話盒函式是程式設計師所撰寫的,其地位類似視窗函式。當使用者操作對話盒上的控制元件時,訊息是傳給對話盒管理器,對話盒管理器再呼叫對話盒函式。在對話盒函式內,只需處理我們感興趣的訊息即可。處理完後,返回 TRUE,代表處理過此訊息;而其餘未處理過的訊息,則返回 FALSE。但是,被處理過的訊息,處理後的情形是什麼,也應在返回時告知對話盒管理器才是。這時必須以 DWL_MSGRESULT 為參數,呼叫 SetWindowLong API 才可以將處理過後的返回值,傳給對話盒管理器。SetWindowLong 原型是:

        INVOKE  SetWindowLong,hWnd,nIndex,dwNewLong

事實上,SetWindowLong 可以設定視窗的許多性質,例如設定返回值、視窗函式位址等等。hWnd 為要設定的視窗代碼,nIndex 是指要設定哪一個性質,可以是下面幾種。而要設定的數值,則是以 dwNewLong 傳給系統。若執行成功,則返回原來的設定值;若失敗,則傳回 0。

風格說  明
GWL_EXSTYLE設定新的延伸風格
GWL_STYLE設定新的風格
GWL_WNDPROC設定新的視窗函式位址
GWL_HINSTANCE設定新的執行實例代碼
GWL_ID設定新的視窗識別碼
GWL_USERDATA設定新的使用者資料
DWL_DLGPROC設定新的對話盒函式位址
DWL_MSGRESULT設定對話盒函式的返回值
DWL_USER設定新的額外資料

在大部分情形下,對話盒函式並不關心返回值。然而,如果要禁止使用者更改某些樹狀檢視的項目名稱時,就得設定 TVN_BEGINLABELEDIT 返回值了。TVN_BEGINLABELEDIT 的返回值有兩種,TRUE 與 FALSE。如果返回 TRUE 表示取消編輯項目名稱,這時程式碼為:

        INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,TRUE

如果返回 FALSE,表示允許編輯。

TVN_ENDLABELEDIT 返回值為 TRUE,則接受編輯框內的文字為新的項目名稱;若返回值為 FALSE,則丟棄編輯框的文字,恢復原項目名稱。

就地編輯時,處理 Enter 鍵與 Esc 鍵

在就地編輯時,如果想以按 Enter 鍵,當作編輯完成並更改項目名稱;以按 Esc 鍵,代表放棄更改恢復原來項目名稱,應該是一種很方便的做法,即使是現在滑鼠當道的今日。然而,編輯框處理按鍵的內定方式,並不能滿足這個要求。在編輯框的視窗函式堙A對於單行的編輯框並不處理 Enter 鍵;對於多行的編輯框 ( 具有 ES_MULTILINE 與 ES_WANTRETURN 風格 ),Enter 鍵的作用式換行。這時可以有兩種方式讓編輯框符合我們的要求:一是自行設計編輯框;二是視窗子類別化 ( window subclass )。顯然前者是太過麻煩了,大部分的人都會採用後者,請參考附錄七。此處,小木偶只需將樹狀檢視所建立的編輯框子類別化,使其能處理 Enter 鍵及 Esc 鍵即可。

不過,不同的地方是,如果樹狀檢視是在對話盒內,對話盒管理器處理 Esc 和 Enter 鍵,並不會將其傳遞至樹狀檢視所建立的編輯框,以致於使按鍵無效。若要解決這個問題,應該在樹狀檢視傳送 TVN_BEGINLABELEDIT 通知碼時,以 TVM_GETEDITCONTROL 為參數,呼叫 SendMessage,取得編輯框代碼,再子類別化編輯框。而在子類別化後的編輯框的視窗函式內,處理 WM_GETDLGCODE 訊息,使其傳回 DLGC_WANTALLKEYS。這可使樹狀檢視中的子類別化函式能處理 Esc 和 Enter 鍵。

綜合「TVN_BEGINLABELEDIT 與 TVN_ENDLABELEDIT 的返回值」與「就地編輯時,處理 Enter 鍵與 Esc 鍵」兩小節,最後處理就地編輯的程式片段,被簡化成下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
;---------------------------------------------------------------------------------------------------
;子類別化的自行撰寫的編輯框視窗函式
new_edit_proc   PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_CHAR
                mov     eax,wParam
        .IF al==VK_RETURN
                INVOKE  SendMessage,hTreeView,TVM_ENDEDITLABELNOW,FALSE,0
        .ELSEIF al==VK_ESCAPE
                INVOKE  SendMessage,hTreeView,TVM_ENDEDITLABELNOW,TRUE,0
        .ELSE
                INVOKE  CallWindowProc,lpOldEditProc,hWnd,uMsg,wParam,lParam
                ret
        .ENDIF
 
.ELSEIF uMsg==WM_GETDLGCODE
                mov     eax,DLGC_WANTALLKEYS
                ret
 
.ELSE
                INVOKE  CallWindowProc,lpOldEditProc,hWnd,uMsg,wParam,lParam
                ret
.ENDIF
                xor     eax,eax
                ret
new_edit_proc   ENDP
;---------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
……
.IF uMsg==WM_NOTIFY
                push    ebx
                ASSUME  ebx:PTR NM_TREEVIEW
                mov     ebx,lParam
                mov     eax,[ebx].hdr.hwndFrom  ;EAX=發出WM_NOTIFY的控制項代碼
    .IF eax==hTreeView
      ;處理TVN_BEGINLABELEDIT通知碼,禁止使用者就地編輯「電腦中的硬碟」、「實體磁碟」、「邏輯磁碟」,但准許編輯檔案
      ;或子目錄就地編輯,不過有限制,新的項目名稱長度在32位元組以內
      .IF [ebx].hdr.code==TVN_BEGINLABELEDIT
                ASSUME  ebx:PTR NMTVDISPINFO
         .IF ([ebx].item.lParam==504d5e43h)||([ebx].item.lParam==4448474ch)||([ebx].item.lParam==44485946h)
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,TRUE                   ;禁止使用者就地編輯「電腦中的硬碟」等三種項目
         .ELSE
                INVOKE  SendMessage,hTreeView,TVM_GETEDITCONTROL,0,0
                mov     hEdit,eax
                INVOKE  SendMessage,eax,EM_SETLIMITTEXT,20h,0                   ;編輯框只能輸入32位元長度
                INVOKE  SetWindowLong,hEdit,GWL_WNDPROC,OFFSET new_edit_proc    ;子類別化編輯框
                mov     lpOldEditProc,eax
         .ENDIF
 
      ;處理TVN_ENDLABELEDIT通知碼,若編輯框內無字元,則恢復原項目名稱;若有字元,則以編輯框內容為項目新名稱
      .ELSEIF [ebx].hdr.code==TVN_ENDLABELEDIT
                INVOKE  GetWindowTextLength,hEdit       ;EAX=編輯框內字元長度
         .IF eax==0
                mov     ecx,FALSE
         .ELSE
                mov     ecx,TRUE
         .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,ecx

拖曳項目 ( 也叫拖拉,drag and drop item )

當使用者把滑鼠游標移至某個項目上,再按住滑鼠左鍵不放,移動滑鼠,這時開始拖曳過程。一般而言,這時滑鼠游標除了原來的白色箭號 ( ) 之外,還會多出一個圖像,稱為拖曳圖像。當滑鼠游標移到目的地後,使用者才放開滑鼠左鍵,完成拖曳動作,滑鼠游標也恢復成原來的白色箭號。底下是拖曳時,程式應有的步驟:

1. 處理 TVN_BEGINDRAG 通知碼

  1. 當開始做拖曳動作時,樹狀檢視會發出 TVN_BEGINDRAG 通知碼給父視窗,父視窗於處理此通知碼時,可以發出 TVM_CREATEDRAGIMAGE 訊息給樹狀檢視,讓樹狀檢視自行建立一個 image list,並且把該項目的圖片加入到 image list 堙A最後把該 image list 的代碼傳回給父視窗,其原型是:

            INVOKE  SendMessage,hTreeView,TVM_CREATEDRAGIMAGE,0,hitem

    wParam 必須為 0,lParam 是被拖曳的項目代碼。如果您不打算在拖曳時顯示圖像,當然可以不做此步驟。又或者如果你不想用項目的圖像,要新建其他圖像也未嘗不可。

  2. 接著要做的是在圖像上指定一個點,做為滑鼠在拖動時,滑鼠熱點指在圖像上的位置。游標的熱點 ( hot spot ) 指的就是當滑鼠點擊時,作用的那一個點,一般是指游標左上角的那一點。我們可以呼叫 ImageList_BeginDrag 指定圖像上的熱點位置,其原型是:

    ImageList_BeginDrag PROTO   himlTrack:DWORD,iTrack:DWORD,dxHotspot:DWORD,dyHotspot:DWORD

    himlTrack 和 iTrack 分別表示 image list 代碼及該 image list 的索引值,這兩項指定了圖像。dxHotspop、dyHotspot 指定圖像的哪一點做為游標的熱點,它們是相對於圖像的左上角,因此如果以左上角為熱點,那麼這兩個數值均設為 0。

  3. 接著是呼叫 ImageList_DragEnter,這個 API 會於指定的位置顯示拖曳圖像,並通知系統開始拖曳了。其原型為:

    ImageList_DragEnter PROTO   hwndLock:DWORD,x:DWORD,y:DWORD

    hwndLock 是擁有拖曳 image list 的視窗代碼,在此處即為樹狀檢視代碼;x、y 分別是圖像的 x 座標與 y 座標,此座標的原點是在該視窗的左上角,並非工作區的左上角,但是對於樹狀檢視而言,並沒有選單、標題欄,所以視窗的左上角也是工作區的左上角。因為在上一步驟,已經呼叫 ImageList_BeginDrag 了,所以系統也已經知道該視窗的拖曳 image list 是哪一個,也知道圖像索引。

  4. 到此,還有一個問題要解決。當我們做拖曳動作時,圖像會隨著滑鼠游標移動而移動,我們必須在父視窗的視窗函式中得到滑鼠位置,才能做出這種效果來,一般的方法是處理 WM_MOUSEMOVE 訊息 ( 參考第五章的說明 )。但是我們是以滑鼠在樹狀檢視堜鴞眸等堙A並不是在父視窗拖曳項目。那麼,父視窗又是如何才能得知滑鼠位置呢?這時候就必須呼叫 SetCapture 了。SetCapture 的原型是:

    SetCapture  PROTO   hWnd:HWND

    hWnd 是視窗代碼,當滑鼠游標移到此視窗的子視窗或控制項時,系統仍會把滑鼠訊息 ( 指 WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTON_UP 等等有關滑鼠輸入的訊息 ) 傳給 hWnd,而不是子視窗或控制項。如果滑鼠游標移到其他視窗 ( 不是 hWnd 視窗,也不是其子視窗或控制項,而是其他程式產生的視窗 ),那麼滑鼠訊息就不會發送到 hWnd 堙A除非滑鼠事先在 hWnd 視窗堣w按下滑鼠左鍵或右鍵,且沒有鬆開,再移到其他視窗上。

2. 處理 WM_MOUSEMOVE 訊息

  1. 到此一步驟時,已經有拖曳圖像,也可以藉由 SetCapture 能夠在父視窗的視窗函式中監視 WM_MOUSEMOVE 訊息,即使滑鼠在控制項堬劓吽C處理 WM_MOUSEMOVE 訊息的首要工作,大概是把拖曳圖像隨著滑鼠游標移動而移動。這個工作可以呼叫 ImageList_DragMove 完成,ImageList_DragMove 的作用就是移動拖曳的圖像其原型是:

    ImageList_DragMove  PROTO   x:DWORD,y:DWORD

    其中 x、y 分別代表相對於樹狀檢視子視窗左上角的 x 座標與 y 座標。對樹狀檢視而言,因沒有選單、標題欄,所以視窗左上角也就是工作區左上角。但是由 WM_MOUSEMOVE 所攜帶的參數 lParam 所得到的座標是相對於對話盒工作區左上角,因此還得做一些轉換,轉換方式就是扣除對話盒工作區左上角到樹狀檢視左上角之 x、y 座標之差距。可以呼叫 GetWindowRect 得到對話盒與樹狀檢視兩視窗的位置,此位置是以螢幕左上角為原點。

  2. 使用者一般的習慣是當拖曳圖像經過某個項目上空時,該項目應該以高亮度顯示。為達此效果,程式先發出 TVM_HITTEST 訊息給樹狀檢視,確定是否經過某個項目的上空;再發出 TVM_SELECTITEM 訊息,使該項目變成高亮度顯示。先說 TVM_HITTEST 訊息,發出 TVM_HITTEST 過程如下:

    INVOKE  SendMessage,hTreeView,TVM_HITTEST,0,ADDR tvhti

    TVM_HITTEST 的 wParam 必須為零,lParam 指向一個稱為 TVHITTESTINFO 的結構體,其欄位是:

    TVHITTESTINFO   STRUCT
    pt              POINT   <>
    flags           DWORD   ?
    hItem           DWORD   ?
    TVHITTESTINFO   ENDS

    呼叫 SendMessage 前必須先把 pt 填入某個點的座標,以測試這個點是否包含在某個項目內,這個座標是以樹狀檢視的工作區左上角為原點。如果樹狀檢視具有捲軸,而且已捲動到某個位置,例如向右捲動時,畫面會向左移,可能會看不見原來的原點,但是此點的座標是以可看見的區域左上角為原點,而不是以原來的原點為原點。如果這個點包含在某個項目內,程式由 SendMessage 返回時,系統會在 TVHITTESTINFO 內的 hItem 填上該項目的代碼,否則填上 NULL,返回值與 hItem 相同。TVHITTESTINFO 的 flags 欄位可以是以下一個或數個數值:

    常 數數值意 義
    TVHT_ABOVE100H 該點在樹狀檢視可見區域外的上方。
    TVHT_BELOW200H 該點在樹狀檢視可見區域外的下方。
    TVHT_NOWHERE1H 該點在可見區域內,但是在最後一個項目的下方。
    TVHT_ONITEM46H 該點在某個項目的圖像或名稱上。
    TVHT_ONITEMBUTTON10H 該點在某個項目的按鈕上。
    TVHT_ONITEMICON2H 該點在某個項目的圖示上。
    TVHT_ONITEMINDENT8H 該點在某個項目的縮排位置上。
    TVHT_ONITEMLABEL4H 該點在某個項目的名稱上。
    TVHT_ONITEMRIGHT20H 該點在可見區域內,某個項目的右方。
    TVHT_ONITEMSTATEICON40H 該點在某個項目的 state icon 上。
    TVHT_TOLEFT800H 該點在樹狀檢視可見區域外的左方。只有該點的 X 座標為負值,才會這樣。
    TVHT_TORIGHT400H 該點在樹狀檢視可見區域外的右方。

  3. 在發送訊息 TVM_SELECTITEM 前,必須先隱藏拖曳圖像,否則會留下難看的痕跡。要隱藏拖曳圖像可以呼叫 ImageList_DragShowNolock,在顯示完高亮度的項目之後,再呼叫一次該 API,以使得拖曳圖像正常顯示。ImageList_DragShowNolock 原型為

    ImageList_DragShowNolock    PROTO   fShow:BOOL

    若 fShow 為 TRUE,表示顯示圖像;FALSE 表示隱藏圖像。返回值為零,表示呼叫失敗;非零表示呼叫成功。

  4. 要使項目變為高亮度顯示,程式要發出 TVM_SELECTITEM 給樹狀檢視,有關 TVM_SELECTITEM 訊息發送方式為:

    INVOKE  SendMessage,hTreeView,TVM_SELECTITEM,flag,hitem

    hitem 是要選擇的項目代碼,flag 可以是以下數值。如果被選擇的項目是某個子項目,而其父項目處於收攏狀態,那麼此訊息會使得父項目展開,同時使被選擇的子項目顯現,在這種情形下,樹狀檢視還會發出 TVN_ITEMEXPANDING 與 TVN_ITEMEXPANDED 兩通知碼給父視窗。如果成功的選定項目,返回 TRUE;否則返回 FALSE。

    flag數值意 義
    TVGN_CARET9H 選定 hitem 項目,並發出 TVN_SELCHANGING 與 TVN_SELCHANGED 通知碼給父視窗。
    TVGN_DROPHILITE8H 由於拖曳目標而重繪 hitem。
    TVGN_FIRSTVISIBLE5H 使 hitem 被選定,並且儘量使其垂直捲動,變成可見區域的第一個項目。但是如果項目太少,且 hitem 在很後面,就無法達成,因此說「儘量」。
    TVSI_NOSINGLEEXPAND8000H 被選定的子項目不展開,必須搭配 TVGN_CARET 使用。

3. 處理 WM_LBUTTONUP 訊息

當使用者放開滑鼠左鍵時,表示結束拖曳動作,系統會發出 WM_LBUTTONUP 訊息,我們必須於此訊息中處理底下的事情,才不會造成錯誤。待這些必要的事處理完後,再依我們的需求,完成要求。

  1. 第一件事是如果在 WM_MOUSEMOVE 中,曾使某個項目變成被選取過,必須發送 TVM_SELECTITEM 訊息,並設為 TVGN_DROPHILITE,但這時候的 lParam 須設為零。如果不這麼做,那麼會發生奇異現象。
  2. 呼叫 ImageList_EndDrag 和 ImageList_DragLeave,告訴系統結束拖曳動作,並呼叫 ImageList_Destroy 銷毀建立的拖曳圖像列表。另外也要呼叫 ReleaseCapture 釋放捕獲的滑鼠游標。
  3. 待上面的制式工作完成,就可以做程式要做的事了,例如把拖曳的項目放在目標項目之下。

樹狀檢視的訊息 ( Message )

樹狀檢視的訊息,均以 TVM_ 為開頭,很明顯是 tree view message 的縮寫。程式可以對樹狀檢視發出訊息,要求樹狀檢視做一些事情。底下來看看樹狀檢視的訊息:

訊息wParamlParam 意   義返回值
TVM_CREATEDRAGIMAGE0 hitem 使樹狀檢視自行建立一個 image list,並且把 hitem 項目的圖片加入到 image list 堙C如果樹狀檢視沒有相關的 image list,則無法使用本訊息,必須自行建立 image list。不再使用拖曳圖像時,程式得自行銷毀其 image list。 成功時返回 image list 的代碼;否則返回 NULL
TVM_DELETEITEM0 hitem 刪除 hitem 項目及其以下的所有子項目,如果 hitem 為 TVI_ROOT 或 NULL 就會刪除所有項目。當項目被刪除時,父視窗會收到 TVN_DELETEITEM。 成功時返回 TRUE;否則返回 FALSE
TVM_EDITLABEL0 hitem 就地編輯 ( in-place editing ) hitem 項目的名稱。此訊息會使樹狀檢視在 hitem 所在位置,建立單行編輯框,以更改項目名稱,並發出 TVN_BEGINLABELEDIT 給父視窗。發送此訊息前,樹狀檢視應該要有輸入焦點。 成功時返回單行編輯框代碼;否則返回 NULL
TVM_ENDEDITLABELNOWfCancel 0 結束就地編輯。fCancel 為 TRUE 時,表示結束就地編輯不儲存;FALSE 表示要儲存。此訊息會使樹狀檢視發出 TVN_ENDLABELEDIT 給父視窗。 成功時返回 TRUE;否則返回 FALSE
TVM_ENSUREVISIBLE0 hitem 確定 hitem 能在樹狀檢視中看見,如果必要,會展開 hitem 的父項目且捲動。如果有展開的動作,就會發出 TVN_ITEMEXPANDING、TVN_ITEMEXPANDED 給父視窗。 如果只有捲動且無項目被展開,返回非零值;否則返回 0
TVM_EXPANDflag hitem 依據 flag 之值,決定 hitem 展開或收攏子項目,flag 可以是
  1. TVE_COLLAPSE:收攏子項目
  2. TVE_COLLAPSERESET:收攏並刪除其子項目,須與 TVE_COLLAPSE 合用,同時 TVIS_EXPANDEDONCE 會被設置。
  3. TVE_EXPAND:展開。
  4. TVE_EXPANDPARTIAL:部份展開。只有 4.70 版之後的 COMCTL32.DLL 可使用,須與 TVE_EXPAND 合用。
  5. TVE_TOGGLE:若項目已展開,則收攏;若項目已收攏,則展開。
展開已展開的項目,被認為是成功的,會返回非零值;但收攏已收攏的項目,卻會返回 0。
當項目被展開,樹狀檢視會發出 TVN_ITEMEXPANDING 和 TVN_ITEMEXPANDED 通知碼給父視窗,並設定 TVIS_EXPANDEDONCE 狀態。
成功時返回非零;否則返回 0
TVM_GETBKCOLOR0 0 取得樹狀檢視的底色,一般返回 COLORREF 值,若返回 -1,表示使用系統預定的底色。 參考左欄
TVM_GETCOUNT0 0 返回項目總數。 參考左欄
TVM_GETEDITCONTROL0 0 取得就地編輯時的編輯框代碼。參考就地編輯的說明。 成功時返回編輯框代碼;否則返回 0
TVM_GETEXTENDEDSTYLE0 0 取得樹狀檢視的延伸風格,參考延伸風格 返回延伸風格
TVM_GETIMAGELISTiImage 0 取得樹狀檢視的 image list 或 state image list。iImage 可以是 TVSIL_NORMAL,表示取得 image list;也可以是 TVSIL_STATE,表示取得 state image list。 返回 image list 代碼
TVM_GETINDENT0 0 取得子項目左邊縮排多少點。 參考左欄
TVM_GETINSERTMARKCOLOR0 0 取得插入記號的顏色。返回時,EAX 的為原有的插入記號顏色。 參考
TVM_GETISEARCHSTRING
TVM_GETITEM0 pitem 取得某個項目的資料。呼叫 SendMessage 後,該項目資料會存在 pitem 所指位址,此位址其實是一個稱為 TV_ITEM 的結構體。在呼叫 SendMessage 前,必須把要取得資料的項目代碼存於 TV_ITEM 的 hItem 欄位,要取得哪些資料要存於 imask 欄位。有關更詳細的用法,請參考取得項目的資料 成功時返回 TRUE;否則返回 FALSE
TVM_GETITEMHEIGHT0 0 取得項目的高度,以點為單位。在樹狀檢視中,每個項目一樣高。 返回值為項目的高度
TVM_GETITEMRECTfItemRect prc 取得圍住項目的矩形大小,以檢視是否能看得見此項目。若 fItemRect 為 TRUE,表示僅取得圍住項目名稱的矩形;若為 FALSE,則取得圍住項目整列的矩形。而矩形的位址是以 prc 指定,其座標以樹狀檢視左上角為原點。在呼叫前 prc 所指 RECT 結構體的 left 欄位為要取的圍住矩形的項目。 如果項目可見且成功取得圍住的矩形,返回 TRUE;否則返回 FALSE。
TVM_GETITEMSTATEhItem stateMask 取得 hItem 項目的某些狀態,要取得的狀態以 stateMask 表示,stateMask 的意義與 TVITEMEX 的 stateMask 欄位相同。 只有被 stateMask 指定且設定的狀態位元為 1
TVM_GETLINECOLOR0 0 取得項目與子項目線段的顏色。要取得按鈕顏色,要用 TVM_GETTEXTCOLOR 訊息。 返回線段顏色,如果沒有指定,返回 CLR_DEFAULT
TVM_GETNEXTITEMflag hitem 取得 flag 所指定的項目代碼,請參考取得項目的資料 請參考取得項目的資料
TVM_GETSCROLLTIME0 0 取得最大捲動時間 ( maximum scroll time )。最大捲動時間是指完成捲動所花的最長時間,以毫秒為單位。 返回值為最大捲動時間。
TVM_GETTEXTCOLOR0 0 若成功,取得項目名稱的顏色,以 COLORREF 形式存於 EAX;若失敗,返回 -1。 參考左欄。
TVM_GETTOOLTIPS0 0 返回時,EAX 為樹狀檢視的子視窗,工具提示 ( tool tip ) 的代碼;若 EAX 為 NULL,表示此樹狀檢視沒有工具提示。建立樹狀檢視時,系統會自動為其建立一個工具提示的子視窗,除非樹狀檢視具有 TVS_NOTOOLTIPS 風格,才不建立工具提示。 參考左欄。
TVM_GETUNICODEFORMAT0 0 返回非零值,表示樹狀檢視使用萬國碼字元;返回 0 值,表示使用 ANSI 字元。 參考左欄。
TVM_GETVISIBLECOUNT0 0 取得能在樹狀檢視中見到完整的項目個數,這個數目可能比數狀檢視的項目數還多,因為系統是以樹狀檢視的高度除以項目高度所得的商,已無條件捨去變整數。 參考左欄。
TVM_HITTEST0 lpht 檢查某個點 ( 其座標以樹狀檢視左上角為原點 ) 是否在某個項目上空。若在某個項目上,返回時,EAX 為項目代碼;否則為 NULL。參考前面 參考左欄。
TVM_INSERTITEM0 lpis 於樹狀檢視中新添加一個項目,lpis 為指向 TVINSERTSTRUCT 結構體位址的指標,詳細資料請參考增添項目 成功時返回項目代碼;否則為 NILL。
TVM_MAPACCIDTOHTREEITEMid 0 每當新增項目時,系統會為此項目指定一個代碼及識別碼 ( accessibility ID ),此訊息可由項目的識別碼取得項目代碼。只能在 XP 及其以上的系統使用。 返回項目代碼。
TVM_MAPHTREEITEMTOACCIDhtreeitem 0 由項目代碼獲得識別碼。只能在 XP 及其以上的系統使用。 返回項目識別碼。
TVM_SELECTITEMflag hitem 選定 hitem 所指定的項目,把它移到可見區域內,或是因拖曳動作而重繪。請參考前面的說明 成功時返回 TRUE;否則返回 FALSE。
TVM_SETAUTOSCROLLINFOuPixPerSec uUpdateTime 設定自動捲動的性質,uPixPerSec 為每秒捲動點數,只能在 Vista 及其以上的系統並且具有 TVS_EX_AUTOHSCROLL 延伸風格的樹狀檢視中使用。 返回 TRUE。
TVM_SETBKCOLOR0 clrBk 設定樹狀檢視的背景顏色為 clrBk,clrBk 的資料形態是 COLORREF,若 clrBk 為 -1,表示使用系統內定值。 返回值為先前的背景顏色。
TVM_SETEXTENDEDSTYLEhwnd messageID 設定樹狀檢視的延伸風格,hwnd 是樹狀檢視代碼,messageID 是要設定的延伸風格,參考上面的說明 設定成功返回 S_OK;其他值表示失敗。
TVM_SETIMAGELISTiImage himl 參考在項目前加入圖片 返回原先的 image list 代碼。
TVM_SETINDENTindent 0 設定子項目相距父項目的縮排的點數,若此點數小於系統預定最小縮排點數,則以系統預定最小縮排點數為準。一般而言,系統預定最小縮排點數為 5 點,但也可能不是。要取得確切的最小縮排點數,可以先發出使 undent 設成 0,發出 TVM_SETINDENT 之後,再發出 TVM_GETINDENT 訊息,返回值即為最小縮排點數。 無。
TVM_SETINSERTMARKfAfter htiInsert 如果想把某個項目拖曳到樹狀檢視的最下面項目之下,這時候如果能在最後一個項目之下出現一個記號表明項目將安插在這堙A是友善界面的做法,這也就是插入記號 ( insert mark ) 孕育而生的原因。TVM_SETINSERTMARK 只能顯現出插入記號並不會真正新添項目,要真正添加項目要發出 TVM_INSERTITEM。TVM_SETINSERTMARK 訊息的 htiInsert 參數是指插入記號會顯示在這個項目所在之處,而 fAfter 為 TRUE,表示插入記號在這個項目之後;若為 FALSE,表示之後。 返回非零,表示成功;否則失敗。
TVM_SETINSERTMARKCOLOR0 clrInsertMark 設定插入記號的顏色,該顏色為 clrInsertMark。返回時,EAX 的為原有的插入記號顏色。 參考左欄。
TVM_SETITEM0 pitem 設定某個項目的屬性,如名稱、圖片等等。pitem 為結構體 TVITEM 的位址,呼叫前 TVITEM 的 hItem 指定要設定的項目代碼,imask 欄位指定要設定的欄位,同時配合相對應的欄位。 成功時返回 TRUE;失敗時返回 FALSE。
TVM_SETITEMHEIGHTcyItem 0 在樹狀檢視堙A每個項目均等高。除非具有 TVS_NONEVENHEIGHT 風格,其高度才能為奇數點。TVM_SETITEMHEIGHT 是用來設定每個項目的高度,新的高度可在 cyItem 堳定,以點為單位。如果 cyItem 為奇數,且不具 TVS_NONEVENHEIGHT 風格,那麼系統會自動使 cyItem 減一。若 cyItem 為 -1,則系統會恢復預設值。 返回原先高度,以點為單位。
TVM_SETLINECOLOR0 clr 設定項目之間的線段顏色,以 clr 表示。若 clr 為 CLR_DEFAULT,則設為系統預設的顏色。要取得按鈕顏色,要用 TVM_SETTEXTCOLOR 訊息。 返回原先顏色。
TVM_SETSCROLLTIMEuScrollTime 0 設定最大捲動時間。uScrollTime 為最大捲動時間,以毫秒為單位,返回原先最大捲動時間。 參考左欄。
TVM_SETTEXTCOLOR0 clrText 設定文字顏色,clrText 文字顏色,返回原先文字顏色。若 clrText 為-1,則系統會恢復預設值。 參考左欄。
TVM_SETTOOLTIPShwndTooltip 0 設定樹狀檢視的子視窗,工具提示,其工具提示代碼存於 hwndTooltip。返回值為原先工具代碼,若返回值為 NULL,則表示原先無工具提示。 參考左欄。
TVM_SETUNICODEFORMATfUnicode 0 若 fUnicode 為非零值,設定樹狀檢視使用萬國碼;若為零,則使用 ANSI。返回值為原先字集。 參考左欄。
TVM_SHOWINFOTIP0 hitem 顯示 hitem 項目的工具提示。很少程式用此訊息,因為大部分的情形下,系統會自動顯示工具提示。 返回 0。
TVM_SORTCHILDRENfRecurse hitem 對指定的 hitem 項目之下的子項目做排序。若 fRecurse 為 TRUE 時,系統會對 hitem 所有階層的子項目做排序;若 fRecurse 為 FALSE 時,僅對下一階層的子項目排序。 成功時返回 TRUE;失敗時返回 FALSE。
TVM_SORTCHILDRENCB0 psort 以程式設計師所定義的 call back 函式排序,此函式稱比較函式,其位址存於 TVSORTCB 結構體中的 lpfnCompare 欄位,而 psort 則為 TVSORTCB 的位址。TVSORTCB 的各欄位是:
TVSORTCB        STRUCT
hParent         HTREEITEM       ?
lpfnCompare     PFNTVCOMPARE    ?
lParam          LPARAM          ?
TVSORTCB        ENDS
hParent 父項目代碼,在此父項目之下一階層的子項目會被排序;第三個欄位,lparam,會傳給比較函式的第三個參數,lParamSort,作為排序方式。比較函式的原型是:
  CompareFunc  PROTO  lParam1:LPARAM, lParam2:LPARAM, lParamSort:LPARAM
其他更細節的部份可參考第 22 章清單檢視的排序部份。
成功時返回 TRUE;失敗時返回 FALSE。

樹狀檢視的通知碼 ( Notifications )

樹狀檢視等通用控制項是以 WM_NOTIFY 訊息與父程式溝通,其訊息中的 wParam 是發出 WM_NOTIFY 的控制項識別碼,不過識別碼可以重複,因此最好還是要檢查發出 WM_NOTIFY 的控制項代碼才好。lParam 是一個指標,此指標指向某個結構體的位址,這個結構體與通知碼有關,不過此結構體的前三個雙字組欄位合稱 NMHDR,其後也可能還包含其他欄位,底下是簡略內容:

NM_CLICK、NM_DBLCLK、NM_KILLFOCUS、NM_SETFOCUS、NM_RCLICK、NM_RDBLCLK、NM_RETURN、NM_OUTOFMEMORY

這八個通知碼是所有通用控制項,包含樹狀檢視,都能發出的,請參考第二十一章的WM_NOTIFY 訊息

NM_SETCURSOR

若滑鼠游標在某個視窗移動且滑鼠未被捕獲時 ( 呼叫 SetCapture 可捕獲滑鼠訊息 ),系統就會把 WM_SETCURSOR 訊息發送給該視窗的視窗函式。但樹狀檢視通常是某個視窗的子視窗,所以樹狀檢視變會發出 WM_NOTIFY 訊息給樹狀檢視的父視窗,而 WM_NOTIFY 訊息的通知碼即為 NM_SETMOUSE。WM_NOTIFY 的 lParam 參數是指向 NMMOUSE 結構體的位址,NMMOUSE 結構體的各欄位是:

NMMOUSE         STRUCT
hdr             NMHDR   <>
dwItemSpec      DD      ?
dwItemData      DD      ?
pt              POINT   <>
dwHitInfo       DD      ?
NMMOUSE         ENDS

TVN_ASYNCDRAW

當樹狀檢視描繪圖示或重疊圖示出錯且具有 TVS_EX_DRAWIMAGEASYNC 延伸風格時,發出此通知碼給父視窗。完成此通知碼後不需返回值,即可返回。此通知碼的 lParam 參數指向 NMTVASYNCDRAW 結構體,其欄位是:

NMTVASYNCDRAW   STRUCT
hdr             NMHDR                   <>
pimldp          IMAGELISTDRAWPARAMS     <>
hr              HRESULT                 ?
hItem           HTREEITEM               ?
lParam          LPARAM                  ?
dwRetFlags      DWORD                   ?
iRetImageIndex  DWORD                   ?
NMTVASYNCDRAW   ENDS

TVN_BEGINDRAG

使用者已經壓住滑鼠左鍵不放,準備拖曳動作時,樹狀檢視就會發出 TVN_BEGINDRAG 通知碼給其父視窗。其 lParam 參數指向 NMTREEVIEW 結構體位址。NMTREEVIEW 結構體舊稱是 NM_TREEVIEW 結構體,其欄位是:

NMTREEVIEW      STRUCT
hdr             NMHDR   <>
action          DWORD   ?
itemOld         TVITEM  <>
itemNew         TVITEM  <>
ptDrag          POINT   <>
NMTREEVIEW      ENDS

hdr 結構體應不用再說明了,請參考第 21 章,WM_NOTIFY 的部份。action 只用於 TVN_ITEMEXPANDING、TVN_ITEMEXPANDED、TVN_SELCHANGING、TVN_SELCHANGED 通知碼,在 TVN_BEGINDRAG 中不使用。itemOld 和 itemNew 兩個結構體,分別是原來項目和新項目的狀態,其中 itemNew 中的 hItem、state 和 lParam 在開始拖曳時,可提供一些資訊 ( TVITEM 請參考 )。ptDrag 可在開始拖曳時,提供滑鼠相對於螢幕左上角的座標。處理完此通知碼,返回值會被系統忽略。若是樹狀檢視具有 TVS_DISABLEDRAGDROP 風格,則其父視窗不能接收到 TVN_BEGINDRAG。至於實際處理拖曳動作的步驟,請參考拖曳項目

TVN_BEGINLABELEDIT、TVN_ENDLABELEDIT

參考就地編輯項目名稱

TVN_BEGINRDRAG

使用者已經壓住滑鼠右鍵不放,準備拖曳動作時,樹狀檢視就會發出 TVN_BEGINDRAG 通知碼給其父視窗。其餘均與 TVN_BEGINDRAG 相同。

TVN_DELETEITEM

當樹狀檢視刪除某個項目時,就會發出 TVN_DELETEITEM 通知碼給父視窗。處理完此通知碼,返回值會被系統忽略。此通知碼的 lParam 指向 NMTREEVIEW 結構體,其 itemOld 中的 hItem、lParam 包含了被刪除項目的資料。

TVN_GETDISPINFO

在大部份時候,樹狀檢視內的項目資料,例如圖示、名稱等等,是由作業系統負責維護,但是也可以交由程式自行維護。如果交由程式自行維護項目的資料,那麼當樹狀檢視要顯示這些資料時,就會發出 TVN_GETDISPINFO 通知碼,要求程式提供這些資料。這些資料應由父視窗的視窗函式,填入 NMTVDISPINFO 結構體內。而 NMTVDISPINFO 結構體的位址則存於 lParam 參數堙C其中 item 欄位是一個稱為 TVITEM 的結構體,TVITEM 堛 imask、hItem、state 和 lParam 必須填入適當的數值。回應完 TVN_GETDISPINFO 通知碼,系統會忽略返回值。

底下是幾種樹狀檢視會發出 TVN_GETDISPINFO 通知碼的情形:

  1. TVITEM 結構體的 pszText 欄位是 LPSTR_TEXTCALLBACK 時,樹狀檢視發出 TVN_GETDISPINFO,以獲得項目名稱,此時, imask 為 TVIF_TEXT。
  2. TVITEM 結構體的 iImage 或 iSelectedImage 欄位是 I_IMAGECALLBACK 時,樹狀檢視要由父視窗獲得 image list 的圖片索引。這時候,如果此項目為被選定的,則在 NMTVDISPINFO 結構體的 TVITEM 堛 imask 欄位會被系統設為 TVIF_SELECTEDIMAGE;若是沒被選定,則為 TVIF_IMAGE。
  3. TVITEM 結構體的 cChildren 欄位是 I_CHILDRENCALLBACK 時,樹狀檢視發出 TVN_GETDISPINFO,以獲得項目之下是否還有子項目,這時候 imask 為 TVIF_CHILDREN。

TVN_GETINFOTIP

具有 TVS_INFOTIP 風格的樹狀檢視,在滑鼠移過某個項目上空時,樹狀檢視會發送 TVN_GETINFOTIP 通知碼給父視窗。此通知碼的 lParam 指向 NMTVGETINFOTIP 結構體位址。NMTVGETINFOTIP 結構體的各欄位是:

NMTVGETINFOTIP  STRUCT
hdr             NMHDR           <>
pszText         LPTSTR          ?
cchTextMax      DWORD           ?
hItem           HTREEITEM       ?
lParam          LPARAM          ?
NMTVGETINFOTIP  ENDS

NMTVGETINFOTIP 結構體中的 pszText 是指向以 0 結尾的字串位址,這個字串將會顯示在工具提示的內容堙CcchTextMax 則是該字串的長度,如果以 0 結尾,則 cchTextMax 會被忽略。hItem 是滑鼠游標所在的項目的項目代碼,由作業系統填好。處理完 TVN_GETINFOTIP 後,返回時,系統忽略返回值。

TVN_ITEMCHANGING、TVN_ITEMCHANGED

當樹狀檢視內的某個項目資料將要改變時,樹狀檢視會發出 TVN_ITEMCHANGING 通知碼給父視窗;已經改變完成後,則發出 TVN_ITEMCHANGED 通知碼。兩者返回時,返回 TRUE 表示拒絕改變;FALSE 表示接受改變。這兩個通知碼的 lParam 指向 NMTVITEMCHANGE 結構體,其欄位是:

NMTVITEMCHANGE  STRUCT
hdr             NMHDR           <>
uChanged        DWORD           ?
hItem           HTREEITEM       ?
uStateNew       DWORD           ?
uStateOld       DWORD           ?
lParam          LPARAM          ?
NMTVITEMCHANGE  ENDS

欄位中的 uChanged 是指改變哪一種資料,雖然說,某個項目資料將要改變前、後,發出 TVN_ITEMCHANGING 或 TVN_ITEMCHANGED,但是只有狀態改變時,才會發出這兩個通知碼,其餘資料改變並不會發出這兩個通知碼,因此 uChanged 只能是 TVIF_STATE。hItem 是狀態遭到改變的項目代碼。uStateNew、uStateOld 是改變後狀態及改變前狀態。

TVN_ITEMEXPANDING、TVN_ITEMEXPANDED

當使用者以滑鼠點擊項目前的時,會使該項目下的子項目展開或收攏起來,在即將展開或收攏起來,尚未顯示新的畫面前,樹狀檢視會發出 TVN_ITEMEXPANDING 通知碼給父視窗;在展開或收攏畫面完成後,則發出 TVN_ITEMEXPANDED 通知碼。這兩個通知碼的 lParam 均指向 NMTREEVIEW 結構體 ( 也稱為 NM_TREEVIEW,其各個欄位可參考前面的說明 ),其中 itemNew 內含展開或收攏的父項目資料,但只有 itemNew 堛 hItem、state 及 lParam 欄位有用。action 為 TVE_EXPAND,表示項目被展開;若為 TVE_COLLAPSE,表示項目被收攏。

TVN_ITEMEXPANDED 的返回值,會被系統忽略。而 TVN_ITEMEXPANDING 的返回值為 TRUE 時,表示禁止展開或收攏;FALSE 表示允許展開或收攏。

TVN_KEYDOWN

當樹狀檢視具有輸入焦點時,使用者按下鍵盤上的按鍵,樹狀檢視會發出 TVN_KEYDOWN 通知碼給父視窗,如果父視窗的視窗函式不處理 TVN_KEYDOWN,而使用者按下的又是可代表文字的按鍵,例如 A、B、C…等按鍵,則樹狀檢視會使輪流使項目名稱第一個字母為該按鍵的項目變為被選定的項目。但也可以使樹狀檢視不進行上述動作,使返回值為非零就能達到此目的。( 返回值必須呼叫 SetWindowLong,並且設定 DWL_MSGRESULT 旗標才能傳回系統 )

TVN_KEYDOWN 通知碼的 lParam 指向一個稱為 NMTVKEYDOWN 結構體 ( 舊稱為 TV_KEYDOWN ),其欄位是:

NMTVKEYDOWN     STRUCT
hdr             NMHDR   <>
wVKey           WORD    ?
flags           DWORD   ?
NMTVKEYDOWN     ENDS

NMTVKEYDOWN 中的 flags 通常是 0。wVKey 是虛擬鍵碼 ( virtual-key codes )。

TVN_SELCHANGED

當使用者做以下兩件事中的任一種,就會使樹狀檢視中的某個項目被選取:

  1. 以滑鼠點擊樹狀檢視中的某個項目
  2. 按下鍵盤上代表文字的任一按鍵 ( 如英文字母或阿拉伯數字鍵,F1∼F12 不在此內 ) 且項目中也有名稱的第一個字恰好為此按鍵所代表的字

當被選取的項目,已經更換成另一個項目後,則發出 TVN_SELCHANGED 通知碼給父視窗。lParam 指向 NMTREEVIEW 結構體,此結構體中的 itemOld 與 itemNew 兩欄位,分別表示原先被選定的項目和新的選定項目。而且 itemOld 與 itemNew 堙A只有 mask、hItem、state 與 lParam 欄位有效可用。action 欄位表示以哪種方式選定項目,可以是下面三種其中之一:TVC_BYKEYBOARD ( 以鍵盤選定 )、TVC_BYMOUSE ( 以滑鼠點擊選定 ) 和 TVC_UNKNOWN ( 未知方式 )。最後,返回值會被系統忽略。

TVN_SELCHANGING

當被選取的項目,將要更換成另一個項目,尚未顯示新選定的項目前,樹狀檢視會發出 TVN_SELCHANGING 通知碼給父視窗。lParam 指向 NMTREEVIEW 結構體,此結構體中的 itemOld 與 itemNew 兩欄位,分別表示原先被選定的項目和新的選定項目。action 欄位表示以哪種方式選定項目,與 TVN_SELCHANGED 相同。處理這個通知碼時,不可將原來被選定或新的選定項目刪除。

處理 TVN_SELCHANGING 後,若返回 TRUE,表示不接受變更選定項目;返回 FALSE,表示接受選定項目。如果 TVN_SELCHANGING 是發送到對話盒的視窗函式,那麼必須呼叫 SetWindowLong API 設定返回值。

TVN_SETDISPINFO

當樹狀檢視需要顯示項目名稱、對項目名稱排序、編輯項目名稱或者當項目名稱改變時,會發送 TVN_SETDISPINFO 通知碼給父視窗,父視窗須填好 lParam 所指的 NMTVDISPINFO 結構體內 item 欄位 imask 所指示的欄位。例如,如果 NMTVDISPINFO.item.imask 為 TVIF_TEXT,那麼父視窗的視窗函式就應該把 NMTVDISPINFO.item.psaText 填入字串位址,以更改項目名稱。

TVN_SINGLEEXPAND

當使用者以滑鼠單擊具有 TVS_SINGLEEXPAND 風格的樹狀檢視中的某個項目時,樹狀檢視會發出 TVN_SINGLEEXPAND 通知碼給父視窗。不具 TVS_SINGLEEXPAND 風格的樹狀檢視,不會發出 TVN_SINGLEEXPAND 通知碼。lParam 指向 NMTREEVIEW 結構體,這個結構體包含所需資料。

返回值為 TVNRET_DEFAULT 時,表示以內定的方式執行展開或收攏,即項目都會被收攏起來,只有被選取項目底下的子項目展開;而且滑鼠點擊某項目一次就能展開子項目,再點擊該項目就收攏。若返回值為 TVNRET_SKIPOLD 時,表示跳過未被選定項目收攏的步驟;若返回值為 TVNRET_SKIPNEW 時,表示跳過被選定項目展開的步驟。


一個範例:HDINFO2

依往例,小木偶撰寫一個樹狀檢視的例子,HDINFO2,展示樹狀檢視的基本用法。HDINFO2 可以把電腦上的實體磁碟當成項目顯示在樹狀檢視內,並把每個實體磁碟分割而成的邏輯磁碟為其子項目。而每個邏輯磁碟的目錄或檔案,為邏輯磁碟的子項目,如下圖:

HDINFO2 的原始程式

底下是資源描述檔,HDINFO2.RC 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "c:\masm32\include\resource.h"
 
#define  RT_MANIFEST     24
 
#define  IDC_TREEVIEW    1600
#define  IDC_TEXT        1601
#define  IDK_RENAME      1602
#define  TVS_NOHSCROLL   0x8000
 
Hd_Info  DIALOG  100,100,200,150
STYLE    WS_CAPTION|WS_VISIBLE|WS_SYSMENU
FONT     10,"新細明體"
CAPTION  "硬碟資訊"
BEGIN
CONTROL "",IDC_TREEVIEW,"SysTreeView32",WS_BORDER|TVS_HASBUTTONS|TVS_HASLINES|TVS_LINESATROOT|TVS_INFOTIP|TVS_EDITLABELS,
         5,5,110,140
LTEXT   "",IDC_TEXT  ,120,5,75,140
END
 
1        RT_MANIFEST MOVEABLE PURE "HDINFO2.EXE.MANIFEST"
 
COMPUTER        ICON    COMPUTER.ICO
HARDDISK        ICON    HARDDISK.ICO
FOLDER          ICON    FOLDER.ICO
FILE            ICON    FILE.ICO

底下是 HDINFO2.EXE.MANIFEST 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<dependency>
  <dependentAssembly>
      <assemblyIdentity type='win32'
                        name='Microsoft.Windows.Common-Controls'
                        version='6.0.0.0'
                        processorArchitecture='*'
                        publicKeyToken='6595b64144ccf1df'
                        language='*'
      />
  </dependentAssembly>
</dependency>
</assembly>

原始碼,HDINFO2.ASM 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDELIB      HD.LIB          ;參考附錄六所製成的動態連結程式庫
 
GetLogicalDriveFromPhysicalDrive        PROTO   :LPSTR
 
IDC_TREEVIEW                            EQU     1600
IDC_TEXT                                EQU     1601
IOCTL_DISK_GET_DRIVE_GEOMETRY_EX        EQU     700a0h
 
MEDIA_TYPE      TYPEDEF DWORD
TVINSERTSTRUCT  TYPEDEF TV_INSERTSTRUCT ;配合1.60版的WINDOWS.INC,此版包含在MASM32 v11
 
;DISK_GEOMETRY、DISK_GEOMETRY_EX這兩個結構體,沒有在1.60版的
;WINDOWS.INC中定義,故在此定義,他們與取得實體磁碟的資料有關
DISK_GEOMETRY           STRUCT
Cylinders               LARGE_INTEGER   <>
MediaType               MEDIA_TYPE      ?
TracksPerCylinder       DWORD           ?
SectorsPerTrack         DWORD           ?
BytesPerSector          DWORD           ?
DISK_GEOMETRY           ENDS
 
DISK_GEOMETRY_EX        STRUCT
Geometry                DISK_GEOMETRY   <>
DiskSize                LARGE_INTEGER   <>
DataOne                 BYTE            ?
DISK_GEOMETRY_EX        ENDS
 
;***************************************************************************************************
.CONST
szDlgName       BYTE    "HdInfo",0      ;對話盒名稱
szIconComputer  BYTE    "COMPUTER",0    ;電腦圖示
szIconHardisk   BYTE    "HARDDISK",0    ;硬碟圖示
szIconFolder    BYTE    "FOLDER",0      ;子目錄圖示
szIconFile      BYTE    "FILE",0        ;檔案圖示
szIconOpnFolder BYTE    "OPENFOLDER",0  ;開啟的子目錄圖示
szRootItem      BYTE    "電腦中的硬碟",0
szHdInfoFmt     BYTE    "實體磁碟 %d",0dh,0ah
                BYTE    "磁柱數:%I64d",0dh,0ah,"磁頭數:%d",0dh,0ah,"每磁軌的磁區數:%d",0dh,0ah
                BYTE    "每磁區位元組數:%d",0dh,0ah,"磁碟容量:%d GB",0
szLogInfoFmt0   BYTE    "磁碟%c:",0dh,0ah,"總共容量:%d GB",0dh,0ah,"剩下%d GB 可用",0
szLogInfoFmt1   BYTE    "磁碟%c:",0dh,0ah,"總共容量:%d MB",0dh,0ah,"剩下%d MB 可用",0
szFileInfoFmt   BYTE    "檔案大小:",0dh,0ah,"%I64d位元組",0dh,0ah,0dh,0ah,0
szDate0Fmt      BYTE    "建檔時間:",0dh,0ah,"yyyy/MM/dd",0
szDate1Fmt      BYTE    "最後存取時間:",0dh,0ah,"yyyy/MM/dd",0
szDate2Fmt      BYTE    "最後修改時間:",0dh,0ah,"yyyy/MM/dd",0
szTimeFmt       BYTE    " HH:mm:ss",0dh,0ah,0dh,0ah,0
szThisIsCmputer BYTE    "這是電腦。",0
szThisIsDrv     BYTE    "這是磁碟機。",0
szThisIsDir     BYTE    "這是目錄。",0
szThisIsFile    BYTE    "這是檔案。",0
szThisIsUnknown BYTE    "不知道這是什麼!",0
szInfoTipTitle  BYTE    "資訊",0
szCantDnDHere   BYTE    "無法拖放到"
;***************************************************************************************************
.DATA
hInstance       HINSTANCE       ?       ;執行實例代碼
hImageList      HANDLE          ?       ;樹狀檢視的 image list 代碼
hTreeView       HANDLE          ?       ;樹狀檢視代碼
hStatic         HANDLE          ?       ;靜態檢視代碼,用於顯示樹狀檢視內的項目資料
hComputer       HTREEITEM       ?       ;根項目代碼
hEdit           HANDLE          ?       ;樹狀檢視進行就地編輯時,編輯框代碼
lpOldEditProc   DWORD           ?
hDragImage      HANDLE          ?
DragMode        DWORD           FALSE   ;是否進行拖曳動作。是為TRUE;否為FALSE
dwCx            DWORD           ?       ;樹狀檢視距離對話盒多遠:X座標
dwCy            DWORD           ?       ;樹狀檢視距離對話盒多遠:Y座標
szPhyHD         BYTE            "實體磁碟 "
szPhyHDNo       BYTE            "0",0
szPhyDrv        BYTE            "\\.\PhysicalDrive0",0
szInfo          BYTE            200 DUP (0)
szPath          BYTE            512 DUP (0)
;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
;新的編輯框視窗函式,僅處理WM_CHAR、WM_GETDLGCODE
new_edit_proc   PROC    USES ebx esi edi hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_CHAR
                mov     eax,wParam
        .IF al==VK_RETURN
                INVOKE  SendMessage,hTreeView,TVM_ENDEDITLABELNOW,FALSE,0
        .ELSEIF al==VK_ESCAPE
                INVOKE  SendMessage,hTreeView,TVM_ENDEDITLABELNOW,TRUE,0
        .ELSE
                INVOKE  CallWindowProc,lpOldEditProc,hWnd,uMsg,wParam,lParam
                ret
        .ENDIF
 
.ELSEIF uMsg==WM_GETDLGCODE
                mov     eax,DLGC_WANTALLKEYS
                ret
.ELSE
                INVOKE  CallWindowProc,lpOldEditProc,hWnd,uMsg,wParam,lParam
                ret
.ENDIF
                xor     eax,eax
                ret
new_edit_proc   ENDP
;---------------------------------------------------------------------------------------------------
;在項目中添加子項目,此項目為一子目錄,所添加的子項目乃是此子目錄內的檔案或子目錄
;輸入:pDir-項目所代表的子目錄路徑
;   hItm-項目代碼
add_item_from_subdir    PROC    USES edi pDir:LPSTR,hItm:HTREEITEM
                LOCAL   w32fd:WIN32_FIND_DATA
                LOCAL   hSearch:HANDLE
                LOCAL   tvis:TVINSERTSTRUCT
                mov     al,0
                mov     edi,pDir
                mov     ecx,MAX_PATH
                repne   scasb
                mov     DWORD PTR[edi-1],2a2e2a5ch      ;"\*.*,0"字串
                mov     BYTE PTR[edi+3],0
                cld
                INVOKE  FindFirstFile,pDir,ADDR w32fd
                mov     hSearch,eax
                cmp     eax,INVALID_HANDLE_VALUE
                je      finish
        .WHILE eax!=0
                mov     ecx,hItm
                mov     tvis.hInsertAfter,TVI_LAST
                mov     tvis.hParent,ecx
                mov     tvis.itemex.imask,TVIF_CHILDREN or TVIF_TEXT or TVIF_IMAGE or TVIF_SELECTEDIMAGE or TVIF_PARAM
                lea     edx,w32fd.cFileName
            .IF (WORD PTR [edx]!=2e2eh)&&(BYTE PTR [edx]!=2eh)
                mov     tvis.itemex.pszText,edx
                mov     tvis.itemex.cChildren,0
                mov     tvis.itemex.iImage,3
                mov     tvis.itemex.iSelectedImage,3
                mov     tvis.itemex.lParam,454c4946h    ;"FILE"字串
            ;檢查是否為子目錄,若是則tvis.itemex欄位中的cChildren、iImage、iSelectedImage分別需修改成1、2、4
                test    w32fd.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
                jz      @f
                mov     tvis.itemex.cChildren,1
                mov     tvis.itemex.iImage,2
                mov     tvis.itemex.iSelectedImage,4
                mov     tvis.itemex.lParam,44425553h    ;"SUBD"字串
@@:             INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
            .ENDIF
                INVOKE  FindNextFile,hSearch,ADDR w32fd
        .ENDW
finish:         INVOKE  FindClose,hSearch
                ret
add_item_from_subdir    ENDP
;---------------------------------------------------------------------------------------------------
;計算項目名稱的長度 ( 項目名稱位址存於EDI ),並存於ECX堙A此長度包含「0」
get_len         PROC
                mov     al,0
                mov     ecx,MAX_PATH
                repne   scasb
                sub     ecx,MAX_PATH
                neg     ecx
                ret
get_len         ENDP
;---------------------------------------------------------------------------------------------------
;取得某項目的路徑名稱,存於szPath字串
;輸入:hItm-某項目的代碼
;   pName-某項目的名稱所在位址
get_path        PROC    USES ebx esi edi hItm:HTREEITEM,pName:LPSTR
                LOCAL   pFullname:LPSTR
                LOCAL   tvi:TVITEM
                LOCAL   buffer[MAX_PATH]:BYTE
                LOCAL   szFullname[512]:BYTE
                cld
                mov     edx,hItm
                mov     tvi.lParam,0
                mov     tvi.hItem,edx
            ;把選定項目名稱移到szFullname最高位址,亦即字串最後面
                mov     edi,pName
                call    get_len
                lea     edi,szFullname
                mov     esi,pName
                add     edi,SIZEOF szFullname
                sub     edi,ecx
                mov     pFullname,edi
                rep     movsb
        .WHILE tvi.lParam!=4448474ch    ;"LGHD"字串
            ;取得父項目代碼
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PARENT,tvi.hItem
                lea     edx,buffer
                mov     tvi.imask,TVIF_TEXT or TVIF_HANDLE or TVIF_PARAM
                mov     tvi.hItem,eax
                mov     tvi.pszText,edx
                mov     tvi.cchTextMax,SIZEOF buffer
            ;取得父項目名稱
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvi
                lea     edi,buffer
                mov     ecx,MAX_PATH
                call    get_len
                mov     edi,pFullname
                sub     edi,ecx
                lea     esi,buffer
                mov     pFullname,edi
                rep     movsb
                mov     BYTE PTR [edi-1],'\'
        .ENDW
                lea     ecx,szFullname
                mov     esi,pFullname
                add     ecx,512
                mov     edi,OFFSET szPath
                sub     ecx,esi
                rep     movsb
                ret
get_path        ENDP
;---------------------------------------------------------------------------------------------------
;獲得檔案資料,包含檔案大小、建檔時間、最後存取日期及時間、最後修改日期及時間,存於szInfo
;輸入:szPath-完整的檔案名稱字串
;輸出:szInfo-檔案資料的字串
;   EAX-szInfo位址
get_file_info   PROC    USES ebx esi edi
                LOCAL   hFile:HFILE
                LOCAL   fi:BY_HANDLE_FILE_INFORMATION
                LOCAL   ft:FILETIME
                LOCAL   syst:SYSTEMTIME
                LOCAL   pszInfo:LPSTR
                LOCAL   dwSizeSzInfo:DWORD
                INVOKE  CreateFile,OFFSET szPath,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,FILE_FLAG_WRITE_THROUGH,0
        .IF eax==INVALID_HANDLE_VALUE
                mov     eax,OFFSET szInfo
                mov     BYTE PTR [eax],0
        .ELSE
                mov     hFile,eax
                mov     pszInfo,OFFSET szInfo
                mov     dwSizeSzInfo,SIZEOF szInfo
                INVOKE  GetFileInformationByHandle,hFile,ADDR fi
                INVOKE  FileTimeToLocalFileTime,ADDR fi.ftCreationTime,ADDR ft
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst
                INVOKE  wsprintf,OFFSET szInfo,OFFSET szFileInfoFmt,fi.nFileSizeLow,fi.nFileSizeHigh
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
            ;顯示建檔日期及時間
                INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szDate0Fmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
                INVOKE  GetTimeFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szTimeFmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
            ;顯示最後存取日期及時間
                INVOKE  FileTimeToLocalFileTime,ADDR fi.ftLastAccessTime,ADDR ft
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst
                INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szDate1Fmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
                INVOKE  GetTimeFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szTimeFmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
            ;顯示最後修改日期及時間
                INVOKE  FileTimeToLocalFileTime,ADDR fi.ftLastWriteTime,ADDR ft
                INVOKE  FileTimeToSystemTime,ADDR ft,ADDR syst
                INVOKE  GetDateFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szDate2Fmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
                INVOKE  GetTimeFormat,LOCALE_USER_DEFAULT,0,ADDR syst,OFFSET szTimeFmt,pszInfo,dwSizeSzInfo
                dec     eax
                add     pszInfo,eax
                sub     dwSizeSzInfo,eax
                INVOKE  CloseHandle,hFile
                mov     eax,OFFSET szInfo
        .ENDIF
                ret
get_file_info   ENDP
;---------------------------------------------------------------------------------------------------
;取得邏輯磁碟資料,包含邏輯磁碟大小及剩餘於大小
;輸入:pLogHdName-邏輯磁碟名,例如「"C:",0」字串
;輸出:szInfo-邏輯磁碟資料的字串
;   EAX-szInfo位址
get_log_drv     PROC    USES ebx pLogHdName:LPSTR
                LOCAL   hAvailable,lAvailable:DWORD
                LOCAL   hTotal,lTotal:DWORD
                LOCAL   hFree,lFree:DWORD
                mov     eax,pLogHdName
                mov     WORD PTR [eax+2],5ch    ;改成「"C:\",0」字串
                INVOKE  GetDiskFreeSpaceEx,pLogHdName,ADDR lAvailable,ADDR lTotal,ADDR lFree
                mov     eax,lTotal
        .IF (eax>=40000000h&&hTotal==0)||(hTotal>0)
                ;超過1GB
                mov     ecx,1000*1000*1000
                mov     ebx,OFFSET szLogInfoFmt0
        .ELSE
                ;不足1GB,改用MB為單位
                mov     ecx,1000*1000
                mov     ebx,OFFSET szLogInfoFmt1
        .ENDIF
                mov     edx,hTotal
                div     ecx
                mov     lTotal,eax
                mov     eax,lAvailable
                mov     edx,hAvailable
                div     ecx
                mov     edx,pLogHdName
                mov     lAvailable,eax
                movzx   eax,BYTE PTR [edx]      ;EAX=邏輯磁碟名稱
                INVOKE  wsprintf,OFFSET szInfo,ebx,eax,lTotal,lAvailable
                mov     eax,OFFSET szInfo
                ret
get_log_drv     ENDP
;---------------------------------------------------------------------------------------------------
;取得實體硬碟的資料,如磁柱數、磁頭數等等
;輸入:pPhyHdName-字串「"實體磁碟 ?",0」的位址
;輸出:szInfo位址-szInfo字串存有實體硬碟的資料,如磁柱數、磁頭數等等
;   EAX-szInfo位址
get_phy_drv     PROC    USES esi edi pPhyHdName:LPSTR
                LOCAL   hPhyDrv:HANDLE
                LOCAL   dwReturnBytes:DWORD
                LOCAL   dgex:DISK_GEOMETRY_EX
                mov     edi,pPhyHdName          ;EDI指向類似「"實體磁碟 0",0」字串
                mov     al,0
                mov     ecx,MAX_PATH
                cld
                repne   scasb
                sub     edi,2
                mov     edx,OFFSET szPhyDrv+17  ;EDX指向「"\\.\PhysicalDrive0",0」中的"0"
                mov     al,[edi]
                mov     [edx],al
                movzx   esi,al
                sub     esi,"0"
                INVOKE  CreateFile,OFFSET szPhyDrv,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \
                        FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0
                mov     hPhyDrv,eax
                INVOKE  DeviceIoControl,hPhyDrv,IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,0,0,\
                        ADDR dgex,SIZEOF dgex,ADDR dwReturnBytes,0
                mov     eax,dgex.DiskSize.LowPart
                mov     edx,dgex.DiskSize.HighPart
                mov     ecx,1000*1000*1000
                div     ecx
                INVOKE  wsprintf,OFFSET szInfo,OFFSET szHdInfoFmt,esi,\
                        dgex.Geometry.Cylinders.LowPart,dgex.Geometry.Cylinders.HighPart,\      ;磁柱數
                        dgex.Geometry.TracksPerCylinder,\                                       ;磁頭數
                        dgex.Geometry.SectorsPerTrack,\                                         ;磁軌數
                        dgex.Geometry.BytesPerSector,eax                                        ;磁區及容量
                INVOKE  CloseHandle,hPhyDrv
                mov     eax,OFFSET szInfo
                ret
get_phy_drv     ENDP
;---------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   hIcon,hInfoTip:HANDLE
                LOCAL   tvis:TVINSERTSTRUCT
                LOCAL   tvhti:TVHITTESTINFO     ;拖曳圖像時,測試熱點在哪一個項目上(發送TVM_HITTEST訊息)
                LOCAL   hMem,hPhyHD:HANDLE,lphMem:LPSTR
                LOCAL   buffer[MAX_PATH]:BYTE
 
.IF uMsg==WM_INITDIALOG
        ;建立ImageList
                INVOKE  ImageList_Create,16,16,ILC_COLOR32 or ILC_MASK,4,0
                mov     hImageList,eax
        ;載入圖示到 image list,並刪除圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconComputer
                mov     hIcon,eax
                INVOKE  ImageList_ReplaceIcon,hImageList,-1,eax ;0-COMPUTER
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,hIcon
                INVOKE  DestroyIcon,hIcon
                INVOKE  LoadIcon,hInstance,OFFSET szIconHardisk
                mov     hIcon,eax
                INVOKE  ImageList_ReplaceIcon,hImageList,-1,eax ;1-HARDDISK
                INVOKE  DestroyIcon,hIcon
                INVOKE  LoadIcon,hInstance,OFFSET szIconFolder
                mov     hIcon,eax
                INVOKE  ImageList_ReplaceIcon,hImageList,-1,eax ;2-FOLDER
                INVOKE  DestroyIcon,hIcon
                INVOKE  LoadIcon,hInstance,OFFSET szIconFile
                mov     hIcon,eax
                INVOKE  ImageList_ReplaceIcon,hImageList,-1,eax ;3-FILE
                INVOKE  DestroyIcon,hIcon
                INVOKE  LoadIcon,hInstance,OFFSET szIconOpnFolder
                mov     hIcon,eax
                INVOKE  ImageList_ReplaceIcon,hImageList,-1,eax ;4-OPEN FILE
                INVOKE  DestroyIcon,hIcon
        ;取得樹狀檢視控制項及編輯框代碼
                INVOKE  GetDlgItem,hDlg,IDC_TEXT
                mov     hStatic,eax
                INVOKE  GetDlgItem,hDlg,IDC_TREEVIEW
                mov     hTreeView,eax
        ;將ImageList與樹狀檢視連結起來
                INVOKE  SendMessage,hTreeView,TVM_SETIMAGELIST,TVSIL_NORMAL,hImageList
        ;在樹狀檢視控制項中加入「電腦中的硬碟」
                mov     tvis.hParent,TVI_ROOT
                mov     tvis.hInsertAfter,TVI_FIRST
                mov     tvis.itemex.imask,TVIF_TEXT or TVIF_IMAGE or TVIF_SELECTEDIMAGE or TVIF_CHILDREN or TVIF_PARAM
                mov     tvis.itemex.pszText,OFFSET szRootItem
                mov     tvis.itemex.iImage,0
                mov     tvis.itemex.iSelectedImage,0
                mov     tvis.itemex.cChildren,1
                mov     tvis.itemex.lParam,504d5e43h            ;"COMP"字串
                INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
                mov     hComputer,eax
        ;取得各實體磁碟中所含的邏輯磁碟代碼
                INVOKE  GetLogicalDriveFromPhysicalDrive,NULL   ;所需資料大小
                INVOKE  GlobalAlloc,GPTR,eax                    ;配置所需記憶體
                mov     hMem,eax
                mov     lphMem,eax
                INVOKE  GetLogicalDriveFromPhysicalDrive,eax
        ;在樹狀檢視堙A添加各個「實體硬碟 X」項目,並在該項目下添加「X:」邏輯磁碟子項目
   .WHILE TRUE
                mov     ecx,lphMem
                add     ecx,17  ;指向"\\.\PhysicalDrive0"字串的「0」
                mov     edx,OFFSET szPhyHDNo
                mov     al,[ecx]
                mov     [edx],al
                add     lphMem,19
        ;設定實體磁碟使用的TVINSERTSTRUCT
                mov     ecx,hComputer
                mov     tvis.hParent,ecx
                mov     tvis.hInsertAfter,TVI_LAST
                mov     tvis.itemex.pszText,OFFSET szPhyHD
                mov     tvis.itemex.cchTextMax,11
                mov     tvis.itemex.iImage,1
                mov     tvis.itemex.iSelectedImage,1
                mov     tvis.itemex.cChildren,1
                mov     tvis.itemex.lParam,44485946h            ;"FYHD"字串
        ;在樹狀檢視控制項中加入「實體磁碟 X」
                INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
                mov     hPhyHD,eax
        ;設定邏輯磁碟使用的TVINSERTSTRUCT
      .WHILE TRUE
                mov     edx,lphMem
      .BREAK .IF BYTE PTR [edx]==5ch            ;檢查此時體磁碟是否還有其他邏輯磁碟
                cmp     BYTE PTR [edx],0
                jz      finish                  ;檢查是否已完成所有實體磁碟及邏輯磁碟
                mov     ecx,hPhyHD
                mov     tvis.itemex.pszText,edx
                mov     tvis.itemex.cChildren,1 ;邏輯磁碟下,還有子項目
                mov     tvis.hParent,ecx
                mov     tvis.itemex.iImage,1
                mov     tvis.itemex.iSelectedImage,1
                mov     tvis.itemex.lParam,4448474ch            ;"LGHD"字串
                INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
                add     lphMem,3                ;指向下一個邏輯磁碟位址
      .ENDW
   .ENDW
finish:         INVOKE  GlobalFree,hMem
 
.ELSEIF uMsg==WM_NOTIFY
                push    ebx
                ASSUME  ebx:PTR NMTREEVIEW
                mov     ebx,lParam
                mov     ecx,[ebx].hdr.hwndFrom  ;ECX=發出WM_NOTIFY的控制項代碼
   .IF [ebx].hdr.hwndFrom==ecx                  ;發出WM_NOTIFY的控制項代碼
      ;處理TVN_SELCHANGED通知碼,當使用者選定某個項目時,先取得項目名稱及lParam,由lParam判斷該項目是「電腦中的硬碟」
      ;、實體磁碟、邏輯磁碟、檔案還是子目錄。如果是前四者,則在靜態控件hStatic中顯示一些資料;否則不顯示資料
      .IF [ebx].hdr.code==TVN_SELCHANGED
                ;取得被選定項目的名稱與lParam,存於tvis.itemex
                mov     edx,[ebx].itemNew.hItem
                lea     eax,buffer
                mov     tvis.itemex.hItem,edx
                mov     tvis.itemex.imask,TVIF_TEXT or TVIF_PARAM
                mov     tvis.itemex.pszText,eax
                mov     tvis.itemex.cchTextMax,SIZEOF buffer
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvis.itemex
         .IF tvis.itemex.lParam==504d5e43h      ;"COMP"字串
                mov     eax,OFFSET szRootItem
         .ELSEIF tvis.itemex.lParam==44485946h  ;"PHYHD"字串
                INVOKE  get_phy_drv,ADDR buffer
         .ELSEIF tvis.itemex.lParam==4448474ch  ;"LGHD"字串
                INVOKE  get_log_drv,ADDR buffer
         .ELSEIF tvis.itemex.lParam==454c4946h  ;"FILE"字串,先取得該檔案的完整路徑,再呼叫get_file_info獲得資料
                INVOKE  get_path,tvis.itemex.hItem,ADDR buffer
                call    get_file_info
         .ELSE
                mov     eax,OFFSET szIconFile+4 ;指向NULL字串
         .ENDIF
                INVOKE  SetWindowText,hStatic,eax
      ;處理TVN_ITEMEXPANDING通知碼,只有邏輯磁碟或子目錄展開或收攏時,才需處理此通知碼。當展開時,需取得該目錄內各
      ;檔案或其內的子目錄為其子項目。當收攏時,發出TVM_DELETEITEM刪去該項目及其下的所有子項目,但這不符合要求,所以
      ;還得把該項目還原,因此先取得該項目的資料、再刪除、再依剛剛得到的資料重新添加該項目
      .ELSEIF [ebx].hdr.code==TVN_ITEMEXPANDING
                mov     edx,[ebx].itemNew.hItem
                lea     eax,buffer
                mov     tvis.itemex.hItem,edx
                mov     tvis.itemex.imask,TVIF_TEXT or TVIF_PARAM or TVIF_SELECTEDIMAGE or TVIF_IMAGE or TVIF_CHILDREN
                mov     tvis.itemex.pszText,eax
                mov     tvis.itemex.cchTextMax,SIZEOF buffer
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvis.itemex    ;取得被選定項目的資料,存於tvis.itemex
         .IF [ebx].itemNew.lParam==44425553h            ;"SUBD"字串
            .IF [ebx].action==TVE_EXPAND
                INVOKE  get_path,tvis.itemex.hItem,ADDR buffer
                INVOKE  add_item_from_subdir,OFFSET szPath,[ebx].itemNew.hItem
            .ELSEIF [ebx].action==TVE_COLLAPSE
get_parents:    INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PARENT,[ebx].itemNew.hItem
                mov     tvis.hParent,eax
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PREVIOUS,[ebx].itemNew.hItem
                or      eax,eax
                jnz     @f
                mov     eax,TVI_FIRST
@@:             mov     tvis.hInsertAfter,eax
                INVOKE  SendMessage,hTreeView,TVM_DELETEITEM,0,[ebx].itemNew.hItem
                INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
            .ENDIF
         .ELSEIF [ebx].itemNew.lParam==4448474ch        ;"LGHD"字串
            .IF [ebx].action==TVE_EXPAND
                INVOKE  add_item_from_subdir,ADDR buffer,[ebx].itemNew.hItem
            .ELSEIF [ebx].action==TVE_COLLAPSE
                jmp     get_parents
            .ENDIF
         .ENDIF
      ;處理滑鼠拖曳動作,建立拖曳圖像、指定熱點、顯示拖曳圖像、使對話盒捕獲滑鼠訊息、設定現在正進行拖曳動作
      .ELSEIF [ebx].hdr.code==TVN_BEGINDRAG
                INVOKE  SendMessage,hTreeView,TVM_CREATEDRAGIMAGE,0,[ebx].itemNew.hItem
                mov     hDragImage,eax
                INVOKE  ImageList_BeginDrag,hDragImage,0,0,0
                INVOKE  ImageList_DragEnter,hTreeView,[ebx].ptDrag.x,[ebx].ptDrag.y
                INVOKE  SetCapture,hDlg
                mov     DragMode,TRUE
      ;處理TVN_GETINFOTIP通知碼,使工具提示能依「電腦中的硬碟」、實體磁碟、邏輯磁碟、檔案或是子目錄而顯示不同的文字
      .ELSEIF [ebx].hdr.code==TVN_GETINFOTIP
                ASSUME  ebx:PTR NMTVGETINFOTIP
         .IF [ebx].lParam==504d5e43h
                mov     eax,OFFSET szThisIsCmputer
         .ELSEIF [ebx].lParam==4448474ch
                mov     eax,OFFSET szThisIsDrv
         .ELSEIF [ebx].lParam==44485946h
                mov     eax,OFFSET szThisIsDrv
         .ELSEIF [ebx].lParam==44425553h
                mov     eax,OFFSET szThisIsDir
         .ELSEIF [ebx].lParam==454c4946h
                mov     eax,OFFSET szThisIsFile
         .ELSE
                mov     eax,OFFSET szThisIsUnknown
         .ENDIF                      
                mov     [ebx].pszText,eax
                ;設定樹狀檢視的子視窗,工具提示,的風格
                INVOKE  SendMessage,hTreeView,TVM_GETTOOLTIPS,0,0
                mov     hInfoTip,eax
                INVOKE  SendMessage,hInfoTip,TTM_SETTITLE,TTI_INFO,OFFSET szInfoTipTitle
                INVOKE  GetWindowLong,hInfoTip,GWL_STYLE
                or      eax,TTS_BALLOON or TTS_CLOSE
                INVOKE  SetWindowLong,hInfoTip,GWL_STYLE,eax
                INVOKE  SendMessage,hInfoTip,TTM_SETMAXTIPWIDTH,0,400
      ;處理TVN_BEGINLABELEDIT通知碼,禁止使用者就地編輯「電腦中的硬碟」、「實體磁碟」、「邏輯磁碟」,但准許編輯檔案
      ;或子目錄,不過有限制,新名稱在32位元組以內
      .ELSEIF [ebx].hdr.code==TVN_BEGINLABELEDIT
                ASSUME  ebx:PTR NMTVDISPINFO
         .IF ([ebx].item.lParam==504d5e43h)||([ebx].item.lParam==4448474ch)||([ebx].item.lParam==44485946h)
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,TRUE
         .ELSE
                INVOKE  SendMessage,hTreeView,TVM_GETEDITCONTROL,0,0
                mov     hEdit,eax
                INVOKE  SendMessage,eax,EM_SETLIMITTEXT,20h,0
                INVOKE  SetWindowLong,hEdit,GWL_WNDPROC,OFFSET new_edit_proc
                mov     lpOldEditProc,eax
         .ENDIF
      ;處理TVN_ENDLABELEDIT通知碼,若編輯框內無字元,則恢復原項目名稱;若有字元,則以編輯框內容為項目新名稱
      .ELSEIF [ebx].hdr.code==TVN_ENDLABELEDIT
                INVOKE  GetWindowTextLength,hEdit               ;EAX=編輯框內字元長度
         .IF eax==0
                mov     ecx,FALSE
         .ELSE
                mov     ecx,TRUE
         .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,ecx
      ;處理TVN_KEYDOWN通知碼,僅處理使用者按下F2鍵,進行就地編輯
      .ELSEIF [ebx].hdr.code==TVN_KEYDOWN
                ASSUME  ebx:PTR NMTVKEYDOWN
         .IF [ebx].wVKey==VK_F2
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_CARET,0
                INVOKE  SendMessage,hTreeView,TVM_EDITLABEL,0,eax
         .ENDIF
      .ENDIF
   .ENDIF
                pop     ebx
                ASSUME  ebx:NOTHING
 
.ELSEIF uMsg==WM_MOUSEMOVE
    .IF DragMode==TRUE
                mov     eax,lParam
                mov     ecx,eax
                and     eax,0ffffh
                shr     ecx,16
                sub     eax,dwCx
                sub     ecx,dwCy
                mov     tvhti.pt.x,eax
                mov     tvhti.pt.y,ecx
                INVOKE  ImageList_DragMove,eax,ecx
                INVOKE  ImageList_DragShowNolock,FALSE
                INVOKE  SendMessage,hTreeView,TVM_HITTEST,NULL,ADDR tvhti
         .IF eax!=NULL
                INVOKE  SendMessage,hTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
         .ENDIF
                INVOKE  ImageList_DragShowNolock,TRUE
    .ENDIF
 
.ELSEIF uMsg==WM_LBUTTONUP
    .IF DragMode==TRUE
                INVOKE  ImageList_DragLeave,hTreeView
                INVOKE  ImageList_EndDrag
                INVOKE  ImageList_Destroy,hDragImage
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
                mov     tvis.itemex.hItem,eax
                INVOKE  SendMessage,hTreeView,TVM_SELECTITEM,TVGN_CARET,eax
                INVOKE  SendMessage,hTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
                INVOKE  ReleaseCapture
                mov     DragMode,FALSE
                mov     esi,OFFSET szCantDnDHere
                lea     edi,buffer
                mov     ecx,SIZEOF szCantDnDHere
                rep     movsb
                mov     tvis.itemex.imask,TVIF_TEXT
                mov     tvis.itemex.pszText,edi
                mov     tvis.itemex.cchTextMax,SIZEOF buffer-SIZEOF szCantDnDHere
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvis.itemex
                INVOKE  MessageBox,hTreeView,ADDR buffer,OFFSET szInfoTipTitle,MB_OK or MB_ICONERROR               
    .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
                INVOKE  ImageList_Destroy,hImageList
                INVOKE  EndDialog,hDlg,NULL
 
.ELSE           ;其他訊息
                mov     eax,FALSE
                ret
.ENDIF
                mov     eax,TRUE
                ret
DlgProc         ENDP
;---------------------------------------------------------------------------------------------------
START:          INVOKE  GetModuleHandle,NULL
                mov     hInstance,eax
                INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,OFFSET DlgProc,NULL
                INVOKE  ExitProcess,eax
                INVOKE  InitCommonControls
;***************************************************************************************************
END             START

以文書處理軟體,輸入上面三個檔案,並存成純文字檔,下載 HDINFO2.RAR,此壓縮檔含有所需的圖示檔 ( 包含 COMPUTER.ICO、FILE.ICO、FOLDER.ICO、HARDDISK.ICO 與 OPENFLD.ICO ) 及動態連結程式庫 ( HD.DLL 與 HD.LIB ),將其解壓縮後,與前面的三個純文字檔放在同一目錄堙C然後輸入下面指令:

E:\HomePage\SOURCE\Win32\HD_Info>rc hdinfo2.rc [Enter]

E:\HomePage\SOURCE\Win32\HD_Info>ml hdinfo2.asm /link hdinfo2.res [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: hdinfo2.asm

***********
ASCII build
***********

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"hdinfo2.obj"
"/OUT:hdinfo2.exe"
"hdinfo2.res"

E:\HomePage\SOURCE\Win32\HD_Info>

這樣就可以產生 HDINFO2.EXE 了。


解說 HDINFO2.ASM

HDINFO2 中,在樹狀檢視加入「電腦中的硬碟」作為根項目、「實體磁碟」作為子項目、「邏輯磁碟」作為孫項目

觀察並整理 HDINFO2 樹狀檢視的項目,分為五種:「電腦中的硬碟」、「實體磁碟」、「邏輯磁碟」、「目錄」與「檔案」,前面三種在 HDINFO2 執行後就由 WM_INITDIALOG 訊息中加入到樹狀檢視堙C添加「電腦中的硬碟」到樹狀檢視塈@為根項目的程式片段在第 390∼400 行,並使其項目代碼保存在 hComputer 堙C值得一提的是在 TVINSERTSTRUCT 中的 TVITEM 結構體中設定了 lParam 欄位為 504d5e43h,亦即「COMP」字串。設定的原因是為了程式方便區分上述五種項目,因此其他的項目:「實體磁碟」、「邏輯磁碟」、「目錄」與「檔案」,也都設定了 lParam,分別為 44485946h (「FYHD」字串 )、4448474ch (「LGHD」字串 )、44425553h (「SUBD」字串 )、454c4946h (「FILE」字串 )。

至於取得電腦內的實體磁碟,及每個實體磁碟被分個成哪些邏輯磁碟的程式碼在第 401∼406。呼叫 GetLogicalDriveFromPhysicalDrive ( 參考附錄六 ) 可獲得每個實體磁碟含有哪些邏輯磁碟,其原型是:

        INVOKE  GetLogicalDriveFromPhysicalDrive,lpBuffer

如果 lpBuffer 為 NULL,則系統會計算出需要多少位元組,以容納資料。如果 lpBuffer 為某個位址,系統會將資料填到 lpBuffer 所指的位址,格式類似底下的樣子:

00156D28  5C 5C 2E 5C 50 68 79 73 69 63 61 6C 44 72 69 76  \\.\PhysicalDriv
00156D38  65 30 00 43 3A 00 45 3A 00 48 3A 00 49 3A 00 5C  e0.C:.E:.H:.I:.\
00156D48  5C 2E 5C 50 68 79 73 69 63 61 6C 44 72 69 76 65  \.\PhysicalDrive
00156D58  31 00 46 3A 00 47 3A 00 4A 3A 00 5C 5C 2E 5C 50  1.F:.G:.J:.\\.\P
00156D68  68 79 73 69 63 61 6C 44 72 69 76 65 32 00 44 3A  hysicalDrive2.D:
00156D78  00 00                                            ..

程式第 407∼444 行,依據上面的資料,把各個實體磁碟加入到「電腦中的硬碟」之下,作為其子項目;再於各個實體磁碟之下,加入所分割出來的邏輯磁碟,作為「電腦中的硬碟」的孫項目。到這堙AWM_INITDIALOG 訊息已處理完成,在樹狀檢視堣w添加了三層項目:「電腦中的硬碟」、「實體磁碟」、「邏輯磁碟」,這三層項目,在任何情形下,都不會被刪除,也不會再新增。

處理 TVN_ITEMEXPANDING 通知碼,加入檔案或目錄作為子項目,或刪除其子項目

接下來,本應該把各邏輯磁碟內的所有檔案與子目錄加入,成為邏輯磁碟的子項目。但這樣會有問題。現今 ( 中華民國 103 年,西元 2014 年 ) 的硬碟已經以 TB 為單位,其內所含檔案多如牛毛,不可勝數,因此 HDINFO2 並不是一開始,就把所有邏輯磁碟底下的所有檔案和子目錄都加進來,這樣可能很沒有效率。

先說一些背景知識。當使用者雙擊某個邏輯磁碟或某個子目錄,或點擊項目前的時,樹狀檢視會把 TVN_ITEMEXPANDING 通知碼傳給對話盒函式,這時候的 NMTREEVIEW 的 action 欄位可能是 TVE_EXPAND 或 TVE_COLLAPSE,分別表示將要展開或收攏。小木偶所採用的方法是,如果是 TVE_EXPAND 才把該邏輯磁碟或子目錄內的檔案或更下一層的子目錄加進來;如果是 TVE_COLLAPSE 時,則把該邏輯磁碟或該子目錄下的子項目移除。因為如果項目處於收攏時,即使該項目下有許多子項目,使用者仍是見不著,乾脆刪除,等到要展開時,才添加進去。至於「電腦中的硬碟」、「實體磁碟」和「邏輯磁碟」,則因數量少,早在 WM_INITDIALOG 訊息中就已加入且不更動,所以不必再處理。若是使用者雙擊某個檔案時,也因為檔案底下不會有子目錄或其他東西,也不處理。基於以上原因,程式第 477∼509 行,處理 TVN_ITEMEXPANDING 通知碼時,僅僅處理邏輯磁碟或子目錄的展開過程,見程式第 488 與 503 行的 .IF/ELSEIF 指令,檢查 TVITEM 的 lParam 欄位,以判斷是邏輯磁碟還是子目錄。整個流程如下面程式片段:

477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
      ;處理TVN_ITEMEXPANDING通知碼,只有邏輯磁碟或子目錄展開或收攏時,才需處理此通知碼。當展開時,需取得該目錄內各
      ;檔案或其內的子目錄為其子項目。當收攏時,發出TVM_DELETEITEM刪去該項目及其下的所有子項目,但這不符合要求,所以
      ;還得把該項目還原,因此先取得該項目的資料、再刪除、再依剛剛得到的資料重新添加該項目
      .ELSEIF [ebx].hdr.code==TVN_ITEMEXPANDING
                mov     edx,[ebx].itemNew.hItem
                lea     eax,buffer
                mov     tvis.itemex.hItem,edx
                mov     tvis.itemex.imask,TVIF_TEXT or TVIF_PARAM or TVIF_SELECTEDIMAGE or TVIF_IMAGE or TVIF_CHILDREN
                mov     tvis.itemex.pszText,eax
                mov     tvis.itemex.cchTextMax,SIZEOF buffer
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvis.itemex    ;取得被選定項目的資料,存於tvis.itemex
         .IF [ebx].itemNew.lParam==44425553h            ;"SUBD"字串
            .IF [ebx].action==TVE_EXPAND
                INVOKE  get_path,tvis.itemex.hItem,ADDR buffer
                INVOKE  add_item_from_subdir,OFFSET szPath,[ebx].itemNew.hItem
            .ELSEIF [ebx].action==TVE_COLLAPSE
get_parents:    INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PARENT,[ebx].itemNew.hItem
                mov     tvis.hParent,eax
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PREVIOUS,[ebx].itemNew.hItem
                or      eax,eax
                jnz     @f
                mov     eax,TVI_FIRST
@@:             mov     tvis.hInsertAfter,eax
                INVOKE  SendMessage,hTreeView,TVM_DELETEITEM,0,[ebx].itemNew.hItem
                INVOKE  SendMessage,hTreeView,TVM_INSERTITEM,0,ADDR tvis
            .ENDIF
         .ELSEIF [ebx].itemNew.lParam==4448474ch        ;"LGHD"字串
            .IF [ebx].action==TVE_EXPAND
                INVOKE  add_item_from_subdir,ADDR buffer,[ebx].itemNew.hItem
            .ELSEIF [ebx].action==TVE_COLLAPSE
                jmp     get_parents
            .ENDIF
         .ENDIF

處理邏輯磁碟或子目錄的展開方式是不同的,邏輯磁碟的項目名稱,必定是像「C:\」這樣的字串,因此只要在第 481∼487 行取得項目名稱即可 ( 尾部再加上「\」及 NULL 字元的過程在 add_item_from_subdir 副程式一開始 ),就可以呼叫副程式,add_item_from_subdir,取得其下的檔案及子目錄做為邏輯磁碟的子項目 ( 第 505 行 )。但是如果是子目錄的話,須先取得完整的路徑名稱才能取得其下的檔案及子目錄,所以在第 490 行,要先呼叫 get_path 副程式,再呼叫 add_item_from_subdir 副程式。

當使用者收攏子目錄時,則刪除其下所有子項目,可以向樹狀檢視發出 TVM_DELETEITEM 訊息。但這個訊息,不僅子項目會被刪除,連項目本身也會遭到刪除。小木偶採用愚笨的方法,先取得項目本身的資料 ( 第 481∼487 )、父項目代碼 ( 第 493∼494 行 ) 以及同一階層的前一個項目代碼 ( 第 495∼499 行 ) ,再刪除項目本身及其下子項目 ( 第 500 行 )。接著,再新增原來的項目 ( 第 501 行 ),這時 TVINSERTSTRUCT 內重要欄位都已是先存好了,所以再新添原項目,並非難事。對於收攏邏輯磁碟的方法,完全和子目錄相同,所以僅僅一個跳躍指令,到 get_parents: 即可 ( 第 507 行 )。

add_item_from_subdir 副程式在第 109∼153 行,第一個參數,pDir,是子目錄的完整路徑名稱位址,程式第 117∼120 行從 pDir 所指位址之處開始搜尋,直到找到子目錄的完整路徑名稱的結尾,然後在結尾之處加上「"\*.*",0」五個位元組。接著搜尋此子目錄內的所有檔案及子目錄,搜尋方法跟以前 DOS 時代一樣,先呼叫 FindFirstFile,找到第一個符合的檔案,系統會將資料填入 WIN32_FIND_DATA 結構體內;然後進入迴圈,每執行一次,就呼叫 FindNextFile,系統搜尋下一個符合的檔案,也會把資料填入 WIN32_FIND_DATA 結構體內。WIN32_FIND_DATA 結構體的各欄位是:

WIN32_FIND_DATA         STRUCT
dwFileAttributes        DWORD           ?               ;檔案屬性
ftCreationTime          FILETIME        <>              ;建檔時間
ftLastAccessTime        FILETIME        <>              ;最後存取時間
ftLastWriteTime         FILETIME        <>              ;最後寫入時間
nFileSizeHigh           DWORD           ?               ;檔案長度,高位址
nFileSizeLow            DWORD           ?               ;檔案長度,低位址
dwReserved0             DWORD           ?               ;保留的欄位
dwReserved1             DWORD           ?               ;保留的欄位
cFileName               BYTE            MAX_PATH DUP (?);不含路徑的檔名
cAlternateFileNa        BYTE            14 DUP (?)      ;短檔名
WIN32_FIND_DATA         ENDS

在 add_item_from_subdir 副程式堙AWIN32_FIND_DATA 重要的欄位是 cFileName 和 dwFileAttributes。cFileName 是檔名字串,利用 LEA 取得其位址 ( 第 133 行 ),再填入到 TVINSERTSTRUCT 結構體的 TVITEM 的 pszText 欄位 ( 第 135 行 )。而 dwFileAttributes 則決定此項目是檔案還是目錄 ( 第 141 行 ),如果是目錄則執行 143∼146,選擇目錄的圖示、設定 lParam 為「SUBD」字串等。如果不是目錄,則選擇檔案圖示、設定 lParam 為「FILE」字串等等。

處理 TVN_SELCHANGED 通知碼,使被選定的項目資料顯示在靜態控件上

HDINFO2 原始程式的第 453∼476 行是用來處理 TVN_SELCHANGED 通知碼。當使用者選取某個項目而使原來被選取的項目變更後,系統會將此通知碼傳給視窗函式。程式第 456∼463 行,將新選取的項目名稱及 lParam 存入 buffer 及 tvis.itemex.lParam 堙C本來發送 TVM_GETITEM 訊息,所得到的項目資料應該放在 TVITEM 結構體內,不過 TVINSERTSTRUCT 內也含有 TVITEM 結構體,因此這奡N偷懶了,沒有另外再定義一個結構體,而直接使用 TVINSERTSTRUCT 堛 TVITEM 了 ( 見第 463 行 )。所得到的 lParam 用來判斷該項目屬於哪一類型,參考下面的流程:

453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
      ;處理TVN_SELCHANGED通知碼,當使用者選定某個項目時,先取得項目名稱及lParam,由lParam判斷該項目是「電腦中的硬碟」
      ;、實體磁碟、邏輯磁碟、檔案還是子目錄。如果是前四者,則在靜態控件hStatic中顯示一些資料;否則不顯示資料
      .IF [ebx].hdr.code==TVN_SELCHANGED
                ;取得被選定項目的名稱與lParam,存於tvis.itemex
                mov     edx,[ebx].itemNew.hItem
                lea     eax,buffer
                mov     tvis.itemex.hItem,edx
                mov     tvis.itemex.imask,TVIF_TEXT or TVIF_PARAM
                mov     tvis.itemex.pszText,eax
                mov     tvis.itemex.cchTextMax,SIZEOF buffer
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvis.itemex
         .IF tvis.itemex.lParam==504d5e43h      ;"COMP"字串
                mov     eax,OFFSET szRootItem
         .ELSEIF tvis.itemex.lParam==44485946h  ;"PHYHD"字串
                INVOKE  get_phy_drv,ADDR buffer
         .ELSEIF tvis.itemex.lParam==4448474ch  ;"LGHD"字串
                INVOKE  get_log_drv,ADDR buffer
         .ELSEIF tvis.itemex.lParam==454c4946h  ;"FILE"字串,先取得該檔案的完整路徑,再呼叫get_file_info獲得資料
                INVOKE  get_path,tvis.itemex.hItem,ADDR buffer
                call    get_file_info
         .ELSE
                mov     eax,OFFSET szIconFile+4 ;指向NULL字串
         .ENDIF
                INVOKE  SetWindowText,hStatic,eax
  1. 如果 lParam 為「504d5e43h」,表示「電腦中的硬碟」,則使 EAX 指向「szRootItem」字串之位址。
  2. 如果 lParam 為「44485946h」,表示實體磁碟,呼叫 get_phy_drv 副程式,get_phy_drv 返回時,EAX 會指向 szInfo 字串的位址,此字串含有實體磁碟的資料。
  3. 如果 lParam 為「4448474ch」,表示邏輯磁碟,呼叫 get_log_drv 副程式,get_log_drv 返回時,EAX 會指向 szInfo 字串的位址,此字串含有邏輯磁碟的資料。
  4. 如果 lParam 為「454c4946h」,表示檔案,先呼叫 get_path 副程式,以取得含路徑的完整檔名,再把剛剛得到的完整檔名做為參數,呼叫 get_file_info,返回時,EAX 會指向 szInfo 字串的位址,此字串含有邏輯磁碟的資料。
  5. 如果 lParam 為「44425553h」,表示目錄,EAX 指向NULL字串。

這些流程分成上面五種情形,HDINFO2 以第 464∼475 行的 .IF/.ELSEIF/.ELSE/.ENDIF 判斷分支,每一種情形最後都使要在靜態控件上顯示的字串位址,存於 EAX。最後 476 行,呼叫 SetWindowText,把 EAX 所指位址的字串顯示在靜態控件上。底下說明這五種不同類型的項目處理方式:

①電腦中的硬碟

程式第 465 行,使 EAX 指向「szRootItem」字串之位址即可。

②實體磁碟

在第 461 行取得項目名稱,這項目名稱類似「"實體磁碟 X",0」的字串,其中的「X」就是第幾個實體磁碟。HDINFO2 把項目名稱當成字串,把此字串位址傳給 get_phy_drv 副程式。get_phy_drv 堛熔 320∼327 行,就是找到第幾個實體磁碟,再把它存到「"\\.\PhysicalDriveX",0」字串的「X」處。「"\\.\PhysicalDriveX",0」字串是待會要呼叫 CreateFile 用到的。CreateFile 並不是僅僅能建立或開啟檔案,還能開啟實體磁碟或邏輯磁碟,請參考附錄六,有關 CreateFile 以及 DeviceIoControl 的說明。這兩個 API 能幫 get_phy_drv 取得實體磁碟的資料,見 HDINFO2.ASM 的第 331∼335 行。get_phy_drv 剩下要做的就是經由 wsprintf 把實體磁碟的磁柱數、磁頭數、每磁軌的磁區數、每磁區位元組數、磁碟容量變成字串,存到 szInfo 字串堙C見 HDINFO2 第 336∼346 行。

③邏輯磁碟

和實體磁碟的情形差不多,把已經取得的項目名稱位址傳給 get_log_drv。不過項目名稱是類似「"C:",0」的樣子,而呼叫 GetDiskFreeSpaceEx 必須使用「"C:\",0」的字串,因此 HDINFO2 的第 285∼286 行做了這項改變。GetDiskFreeSpaceEx 的原型是:

        INVOKE  GetDiskFreeSpaceEx,lpDirectoryName,lpFreeBytesAvailable,lpTotalNumberOfBytes,lpTotalNumberOfFreeBytes

GetDiskFreeSpaceEx 能獲得邏輯磁碟的可用大小、全部大小、剩餘大小。第一個參數,lpDirectoryName,是目錄名稱的位址,例如「"C:\",0」,如果此參數為 0,則使用現在磁碟的根目錄。lpFreeBytesAvailable、lpTotalNumberOfBytes、lpTotalNumberOfFreeBytes 均為位址指標,分別指向 64 位元的正整數,分別代表使用者可用大小、磁碟全部大小、剩餘大小,單位均為位元組。因為每個使用者執行時,權限不同,因此使用者可用大小並不一定等於剩餘大小。也因如此,使用者也需要對 lpDirectoryName 所指向的目錄具有 FILE_LIST_DIRECTORY 的權限才可以。如果呼叫成功,傳回非零值;如果失敗,傳回 0,可呼叫 GetLastError 得到錯誤碼。

另外 lpFreeBytesAvailable、lpTotalNumberOfBytes、lpTotalNumberOfFreeBytes 均指向 64 位元的整數,使用方式如下:

281
282
283
284
285
286
287
get_log_drv     PROC    USES ebx pLogHdName:LPSTR
                LOCAL   hAvailable,lAvailable:DWORD
                LOCAL   hTotal,lTotal:DWORD
                LOCAL   hFree,lFree:DWORD
                mov     eax,pLogHdName
                mov     WORD PTR [eax+2],5ch    ;改成「"C:\",0」字串
                INVOKE  GetDiskFreeSpaceEx,pLogHdName,ADDR lAvailable,ADDR lTotal,ADDR lFree
在第 282∼284 行分別定義了三組變數,每一組都是由兩個雙字組所構成 ( 雙字組是 double word,大小是 32 位元,兩個雙字組就是 64 位元 ),用來接收 GetDiskFreeSpaceEx 傳來的 64 位元資料。以磁碟全部大小為例,GetDiskFreeSpaceEx 會把磁碟全部大小存到 hTotal、lTotal 堙AhTotal 和 lTotal 分別是位址較高的雙字組和較低的雙字組,而 GetDiskFreeSpaceEx 的第三個參數,只需要指向到較低的雙字組位址,GetDiskFreeSpaceEx 會自行把位址較高的 hTotal 填好,見第 287 行。

④檔案

要得到某個檔案的資料,一定得知道這個檔案的完整檔名,這裡所謂的完整檔名指的是包含路徑的名稱,例如「C:\WINDOWS\NOTEPAD.EXE」就是完整的檔名,其中「NOTEPAD.EXE」是檔名,在「.」左邊的「NOTEPAD」是主檔名,「EXE」是副檔名,「C:\WINDOWS\」為路徑名。而 HDINFO2 的樹狀檢視堙A選取某個項目,只能得到檔名,不能得到路徑名稱,因此第 470 行確定使用者選取的項目是檔案後,便於 471 行呼叫 get_path 得到路徑名。get_path 會去計算各碀的項目名稱,最後把完整檔名記錄於 szPath 字串堙C

剛進入 get_path 副程式時,177∼185 行是把項目名稱移到 szFullname 字串的最高位址。szFullname 是一個 512 位元組長的字串,當使用者選定項目時,HDINFO2 上不知道前面路徑有多少,因此先把前面低位址的地方留給路徑名,最後高位址留給檔名。第 179 行呼叫 get_len 就是用來計算項目名稱有多少位元組,存於 ECX 後返回,然後把 szFullname 之位址加上 szFullname 大小 ( 第 182 行 ),再減去 ECX ( 第 183 行 ),就可以知道項目名稱應該由 szFullname 字串的哪一個位址開始填入,最後以「rep movsb」指令移動項目名稱即可。參考下面程式碼:

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
;取得某項目的路徑名稱,存於szPath字串
;輸入:hItm-某項目的代碼
;   pName-某項目的名稱所在位址
get_path        PROC    USES ebx esi edi hItm:HTREEITEM,pName:LPSTR
                LOCAL   pFullname:LPSTR
                LOCAL   tvi:TVITEM
                LOCAL   buffer[MAX_PATH]:BYTE
                LOCAL   szFullname[512]:BYTE
                cld
                mov     edx,hItm
                mov     tvi.lParam,0
                mov     tvi.hItem,edx
            ;把選定項目名稱移到szFullname最高位址,亦即字串最後面
                mov     edi,pName
                call    get_len
                lea     edi,szFullname
                mov     esi,pName
                add     edi,SIZEOF szFullname
                sub     edi,ecx
                mov     pFullname,edi
                rep     movsb
第 184 行的 pFullname 是用來記錄每個目錄名稱,應從 szFullname 字串的哪個位址開始填入,但不是由 szFullname 的相對起始位址算起,而是「絕對」位址。每往上一層項目,pFullname 就會減掉項目名稱的長度。不過一開始,pFullname 是指向 szFullname 選定項目的位址 ( 第 184 行 )。

接下來進入一個迴圈 ( 第 186∼205 行 )。這個迴圈,每次執行時,先取得父項目的代碼 ( 第 188 行 ),然後依代碼取得父項目的名稱 ( 第 189∼195 行 ),存於 buffer 字串 ( 第 189、192 行 ),再呼叫 get_len 計算名稱長度 ( 第 196∼198 行 ),決定要移到 szFullname 的哪一個位址 ( 第 199∼200 行 ),記錄在 pFullname 後 ( 第 202 行 ),最後再把父項目名稱移到此位址上 ( 第 203 行 ) 並加上「\」,作為目錄與目錄之間的分隔記號 ( 第 204 行 )。這個迴圈將一直執行,直到某個父項目是邏輯磁碟為止,要檢查邏輯磁碟,可以檢查項目的 lParam,如果是 4448474CH ( 4448474CH 的 ASCII 碼換成英文字母就是「LGHD」字串 ),就是邏輯磁碟。如下:

186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
        .WHILE tvi.lParam!=4448474ch    ;"LGHD"字串
            ;取得父項目代碼
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_PARENT,tvi.hItem
                lea     edx,buffer
                mov     tvi.imask,TVIF_TEXT or TVIF_HANDLE or TVIF_PARAM
                mov     tvi.hItem,eax
                mov     tvi.pszText,edx
                mov     tvi.cchTextMax,SIZEOF buffer
            ;取得父項目名稱
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvi
                lea     edi,buffer
                mov     ecx,MAX_PATH
                call    get_len
                mov     edi,pFullname
                sub     edi,ecx
                lea     esi,buffer
                mov     pFullname,edi
                rep     movsb
                mov     BYTE PTR [edi-1],'\'
        .ENDW

get_path 的最後幾行,是把存在 szFullname 字串堛漣嗾蒻犰W移到 szPath 堙C在 szFullname 字串堛漣嗾蒻犰W實際上不一定是從 szFullname 所在位址一開始存放,其存放位址是記錄在 pFullname 變數堙C第 207 行就是取得完整檔名的真正存放位址。至於完整檔名的長度,則是記錄在 ECX 堙A它是由 szFullname 的最後位址減去完整檔名的真正存放位址 ( 第 206、208、210 三行 )。程式碼如下:

206
207
208
209
210
211
212
213
                lea     ecx,szFullname
                mov     esi,pFullname
                add     ecx,512
                mov     edi,OFFSET szPath
                sub     ecx,esi
                rep     movsb
                ret
get_path        ENDP
取得完整檔名後,即可返回對話盒函式。接著執行第 472 行呼叫 get_file_info 副程式,得到檔案資料,包含檔案大小與檔案建立時間、最近修改時間、最近存取時間。

在 Win32 系統堙A要獲得檔案建立時間、最近修改時間、最近存取時間,可以呼叫 GetFileTime、GetFileInformationByHandle 等 Win32 API,底下是 GetFileTime API 的原型:

        INVOKE  GetFileTime,hFile,lpCreationTime,lpLastAccessTime,lpLastWriteTime

GetFileTime 的後三個參數,lpCreationTime、lpLastAccessTime、lpLastWriteTime 分別是 hFile 所代表檔案的建立時間、最近存取時間、最近修改時間,三者均為一個位址指標,指向一個稱為 FILETIME 結構體,此欄位儲存了檔案時間,有關 FILETIME 結構體的詳細說明,請參考附錄七 FILETIME 的說明。但是在 HDINFO2 堙A除了要得到檔案時間外,還想獲得其他資料,如檔案大小、屬性等,因此僅呼叫 GetFileTime 是不夠的。所以小木偶在 HDINFO2 堜I叫 GetFileInformationByHandle 而不是 GetFileTime,以獲得檔案較多的資料,GetFileInformationByHandle 的原型是:

        INVOKE  GetFileInformationByHandle,hFile,lpFileInformation

呼叫 GetFileInformationByHandle 前,須於 hFile 指定檔案代碼,並將 lpFileInformation 填好。lpFileInformation 參數是一個指向 BY_HANDLE_FILE_INFORMATION 結構體的位址指標,呼叫完成後系統會於此位址填入 hFile 的資料,BY_HANDLE_FILE_INFORMATION 的欄位是:

BY_HANDLE_FILE_INFORMATION      STRUCT
dwFileAttributes                DWORD           ?
ftCreationTime                  FILETIME        <>
ftLastAccessTime                FILETIME        <>
ftLastWriteTime                 FILETIME        <>
dwVolumeSerialNumber            DWORD           ?
nFileSizeHigh                   DWORD           ?
nFileSizeLow                    DWORD           ?
nNumberOfLinks                  DWORD           ?
nFileIndexHigh                  DWORD           ?
nFileIndexLow                   DWORD           ?
BY_HANDLE_FILE_INFORMATION      ENDS

底下說明 BY_HANDLE_FILE_INFORMATION 的各欄位:

⑤目錄

如果是目錄,則直接指向空字串,使靜態空件沒有資料可顯示。( 當然,如果要顯示目錄的資料,也不是做不到,請參考附錄七附註:目錄的時間 )


到第二十三章回到首頁到第二十五章