天天看點

Windows Shell程式設計-第五章.浏覽檔案夾

第五章 浏覽檔案夾

         我在第二章中給出了檔案夾的概覽和它在Windows Shell中的地位,在這一章中我們打算更詳細地讨論它們。我們主要集中精力闡述涉及檔案夾所有層面的Shell函數,以及保證所有操作順利進行的潛在機理。是以,我們需要深入研究兩個起着非常重要作用的概念:快捷方式和PIDLs。前者是下一章的題目,在這一章中我們将研究PIDLs,其中包括:

         SHBrowseForFolder()函數的用途

    關于PIDLs進一步的讨論,以及怎樣使用PIDLs

    虛拟檔案夾和位置

    怎樣獲得檔案夾的設定

我們将要論述的例子包含了一個增強版本的API函數SHBrowseForFolder(),一些使它更容易同PIDLs一道工作的輔助函數,以及一些怎樣枚舉某些指定位置(如,‘發送到’,‘Favorites’,以及‘我的資料’)内容的樣例程式。

選擇檔案夾

         讓我們先從各種檔案夾選擇方法開始我們的讨論。對于讓使用者能夠從特定驅動器上選擇特殊目錄的應用程式,這是一個普通的需求。Windows3.x API沒有為這提供任何内建的工具,是以必須建立自己的輔助函數,然而,有一項通用技術可以使用,它由修改通用對話框模版組成,例如删除象清單框那樣的包含檔案名的控件。

         然而,推出這個方案到Win32有一個障礙:你必須抛棄新探測器風格的使用者界面,而仍然忠實地使用老界面:

Windows Shell程式設計-第五章.浏覽檔案夾

在Win32平台上,探測器風格的‘打開’對話框是一個單一實體,其中的任何控件都是不能擺脫掉的(比如:檔案清單框)。

         要選擇采用老的Windows3.x界面,另一個選擇是安排一個VC++顯示方式的對話框到新項目中來請求一個特殊的檔案夾。

Windows Shell程式設計-第五章.浏覽檔案夾

跳出Win32關于GetOpenFileName()函數的資料,能夠發現更多的東西。

更現代的方法

從Wondwos95 開始,Win32 SDK 就包含了浏覽檔案夾的系統解決方案:這個函數稱為SHBrowseForFolder()。其主要的特征是使用類似于我們已知的和探測器鐘情的樹觀察:

Windows Shell程式設計-第五章.浏覽檔案夾

與前兩章中我們測試過的函數一樣,SHBrowseForFolder()函數有一個簡單的原型,但是,它實際上包含了一個帶有大量設定和标志的結構,可以把這個函數看作是檔案夾的中心函數,其目的之一就是使我們能夠在桌面命名空間中選擇可用的檔案夾。

SHBrowseForFolder()函數的原型

         現在看一下SHBrowseForFolder()的原型,它聲明在shlobj.h中:

        LPITEMIDLIST WINAPI SHBrowseForFolder(LPBROWSEINFO lpbi);

參數隻有一個BROWSEINFO結構的指針,它的聲明也在同一個檔案中:

typedef struct _browseinfo

{

HWND hwndOwner;

LPCITEMIDLIST pidlRoot;

LPSTR pszDisplayName;

LPCSTR lpszTitle;

UINT ulFlags;

BFFCALLBACK lpfn;

LPARAM lParam;

int iImage;

} BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;

成員的說明如下表:

名稱 描述
hwndOwner 擁有這個對話框的視窗Handle
pidlRoot 被表述層次對象的根節點辨別。是一個PIDL。
pszDisplayName 必須是一個已配置設定緩沖區的指針,它将包含選擇對象的顯示名。
lpszTitle 必須是一個緩沖區指針,包含一個作為樹觀察标題的串。
ulFlags 指定外觀和視窗行為(後面将介紹有效的值)。
Lpfn 用于鈎住對話框的回調函數。
lParam 32位傳遞給回調函數的客戶資料。通常是一個指針或Handle。
Iimage 包含選中檔案夾或檔案的圖示索引。是相對系統圖像清單的索引。

調用SHBrowseForFolder()最簡單的方法是:

BROWSEINFO bi;

ZeroMemory(&bi, sizeof(BROWSEINFO));

bi.hwndOwner = hDlg;

LPITEMIDLIST pidl = SHBrowseForFolder(&bi);

這段代碼顯示一個前面看到過的對話框,并且恢複選中檔案夾的PIDL。如果檔案夾有一個對應的路徑,你可以通過下述代碼獲得:

TCHAR szPath[MAX_PATH] = {0};

SHGetPathFromIDList(pidl, szPath);

Msg(szPath);

有幾個有趣的結果與使用SHBrowseForFolder()函數有關。下面給出概述,在整個下一節我們都将詳細地讨論這些問題:

         這個函數透明地處理PIDLs和路徑名

         這個函數允許浏覽特殊的系統檔案夾

         這個函數傳回大量的資訊,是SHGetFileInfo()所不能的。

         對話框稍微可以客戶化,這總是一個好消息。

SHBrowseForFolder()函數的用法

         SHBrowseForFolder()函數所能做的事情由BROWSEINFO結構的ulFlags成員所限制,其合法的值由下述标志的組合構成:

标志 描述
BIF_RETURNONLYFSDIRS 如果設定,僅在使用者選擇了檔案系統的目錄後,OK按鈕才被允許。例如,你選擇‘網路上的芳鄰’節點,如果這個标志設定,OK按鈕是灰的。
BIF_DONTGOBELOWDOMAIN 不顯示網絡檔案夾,僅有域名節點。
BIF_STATUSTEXT 對話框模版含有可以顯示任何文字的标簽,特别是在子類化這個對話框視窗後(後面将詳細講解)。
BIF_EDITBOX 這是Shell 4.71版以後的新特征,它允許一個編輯框,在這裡可以手動輸入檔案夾。
BIF_VALIDATE 這是Shell 4.71版的另一個新特征,它是對BIF_EDITBOX标志功能的補充。如果你設定了這個标志,并且子類化了這個對話框,則使用者每次在編輯框鍵入和确認一個不正确的檔案或檔案夾名時,你都能收到通知(後面将詳細講解)。
BIF_BROWSEFORCOMPUTER 允許使用者僅選擇計算機名。浏覽正常發生,但是OK按鈕總是灰的,除非選擇了計算機名。
BIF_BROWSEFORPRINTER 與上相同,但是是列印機。
BIF_BROWSEINCLUDEFILES 如果這個标志設定,不管其它标志如何,在樹觀察中都顯示檔案名,而不僅僅是檔案夾名。這就提供了設定對話框顯示系統中所有列印機或可用字型的機會。

在調用SHBrowseForFolder()函數時,有兩種方法來客戶化最終對話框的外觀,經由回調函數子類化這個視窗更有力一些,我們将在這一章的以後部分讨論這個内容。獲得有限程度客戶化的較簡單方法是修改樹觀察上面的文字。BROWSEINFO結構的lpszTitle成員負責這一點。它聲明為一個指針,是以,你必須傳遞一個有效的記憶體緩沖區:

TCHAR szBuf[MAX_PATH] = {0};

lstrcpy(szBuf, __TEXT("Choose a folder:"));

bi.lpszTitle = static_cast<LPCSTR>(szBuf);

對pszDisplayName成員,也一樣,它是一個傳回緩沖區。如果你對選中檔案夾的顯示名感興趣,就需要傳遞一個有效的緩沖區,首先聲明或配置設定它,然後 把指針指派到pszDisplayName。

TCHAR szDisp[MAX_PATH] = {0};

bi.pszDisplayName = static_cast<LPSTR>(szDisp);

函數認為pszDisplayName至少有MAX_PATH位元組尺寸。

         正如前幾章說明的,檔案夾的顯示名是探測器用來顯示檔案夾的名字。例如,(C:)的顯示名是C:/。

函數傳回了什麼

技術上講,函數傳回的是PIDL,它辨別一個選中的檔案或檔案夾。如果‘取消’了對話框,函數傳回NULL,非常簡單。然而,這個函數還能夠通過傳遞的BROWSEINFO結構傳回其它有用的資訊。這一點的特殊例子是包含選中對象的顯示名(上面已經提到了),和代表它的圖示。

擷取檔案夾的圖示

         即使SHBrowseForFolder()看起來似乎正在重複我們已經從SHGetFileInfo()函數獲得的功能。然而,就獲得和顯示圖示仍然有相當的工作需要做。

    在函數傳回時,BROWSEINFO結構的iImage成員含有一個數字,它是圖示在系統圖像清單中的位置索引。因而,如果想要繪制圖示—或更簡單,想要它的HICON Handle—你就必須首先獲得這個圖像清單的Handle。

    在前一章中已經講到了怎樣取得圖示,但是,采用這裡的方法要容易一些。如果使用SHGFI_ICON和标志調用SHGetFileInfo(),并且設定了SHGFI_SYSICONINDEX,函數則傳回系統圖像清單的Handle。

HICON SHGetSystemIcon(int iIconIndex)

{

SHFILEINFO sfi;

ZeroMemory(&sfi, sizeof(SHFILEINFO));

// 不指定檔案名,因為我們隻想要一個Handle...

HIMAGELIST himl = reinterpret_cast<HIMAGELIST>(SHGetFileInfo(

"*.*", 0, &sfi, sizeof(SHFILEINFO), SHGFI_ICON | SHGFI_SYSICONINDEX));

HICON hIcon = ImageList_ExtractIcon(0, himl, iIconIndex);

return hIcon;

}

上面的代碼是一個輔助例程,給定一個索引,傳回系統圖像清單中對應的圖示。要運作這段代碼需要包含shellapi.h,和通過調用InitCommonControls()或InitCommonControlsEx()初始化公共控件庫。就象附錄A中讨論的那樣,第一個方法适用于老版本的Shell,第二才被推薦到Shell 4.71及其以後的版本。

使用回調函數

         有趣的是SHBrowseForFolder()函數要求一個回調函數。要子類化由這個函數建立的對話框,你需要指派一個有效的函數指針到BROWSEINFO的lpfn字段。這個指針必須指向有如下原型的函數:

int CALLBACK BrowseCallbackProc(HWND hwnd,

UINT uMsg,

LPARAM lParam,

LPARAM dwData);

其中hwnd是被鈎住視窗的Handle,uMsg接收到的消息。lParam是一個值,根據uMsg它有不同的意義,而最後這個dwData是使用者定義的資料—與你通過BROWSEINFO的lParam成員指定的資料相同。如果你需要回調函數在調用程式建立的資料上工作,而不是使用全程變量,應該使用一個32位值來填寫BROWSEINFO結構的lParam成員,并且保證它自動地經由dwData變量傳遞給回調函數。為了适合多個資料的傳遞,可以使用指針,更好地,配置設定一個Handle記憶體塊,鎖定它,封包所有東西,解鎖,然後把它存入lParam字段。下圖顯示了回調函數設定的情形:SHBrowseForFolder()調用了你所定義的函數,傳送一些資料和通知某些事件。

Windows Shell程式設計-第五章.浏覽檔案夾

可以感覺的事件

         由SHBrowseForFolder()建立的對話框可以通知回調函數下列事件:

        對話框初始化完成

        選擇已經改變

        使用者在編輯框中鍵入了無效檔案或檔案夾。

通過發送下面消息完成這些功能:

BFFM_INITIALIZED

BFFM_SELCHANGED

BFFM_VALIDATEFAILED

這些消息由回調函數通過uMsg參數接收。每一個消息都在lParam變量帶有一個LPARAM類型的值。現在就讓我們逐個消息地觀看一下lParam是怎樣配置的:

消息 lParam 意義
BFFM_INITIALIZED 無用消息—它總是NULL,這個消息在對話框視窗過程完成WM_INITDIALOG後發送。
BFFM_SELCHANGED 指向新選中檔案夾的辨別符清單,注意,就象其他Windows控件一樣,在選擇已經改變時通知改變事件。
BFFM_VALIDATEFAILED 指向編輯框的目前内容。就象它标志的那樣,這個消息僅僅在Shell 4.71中支援。由傳回0,回調函數可以強迫浏覽對話框關閉,傳回1,對話框保持活動。

可以發送的消息

         有幾個回調函數可以發送給對話框視窗的消息,使它能執行一定的活動。它們是:

消息 描述
BFFM_ENABLEOK 根據lParam值,允許或禁止OK按鈕。如果非零,按鈕允許,此時可以确認目前選中的檔案夾。wParam 沒有用。
BFFM_SETSELECTION 選擇特殊檔案或檔案夾。lParam中存儲一個指向PIDL或路徑名的指針,wParam表示怎樣解釋這個指針。FALSE說明是一個PIDL,TRUE說明路徑名。
BFFM_SETSTATUSTEXT 設定你提供的文字到對話框的狀态區域。實際文字由lParam指向,WParam無用。

這些消息正常地使用SendMessage()函數發送,通過組合它們,你可以實際地增強SHBrowseForFolder()的行為。

客戶化使用者界面

         通過使用回調函數,你可以介入和改變對話框的使用者界面,因而,使它可以更好地适應你的需求。例如,你不希望?(幫助)按鈕在标題條上出現,或使某些控件具有更顯著的3D外觀等。下面内容将說明這些功能是怎樣實作的

删除關聯幫助按鈕

         從标題條中删除關聯幫助按鈕是一個關于視窗顯示風格的簡單問題:你隻需要關閉處使Windows繪制和處理它的訓示位即可。在擴充風格的任何視窗設定了WS_EX_CONTEXTHELP位時,這個按鈕就顯示。

    擴充風格首先在Windows3.x中提出,然後在Windows95的SDK版本中得到加強。在正常風格和擴充風格之間的唯一不同在于它們占用的存儲區域,而不是概念上的差别。

         你需要使用不同于通路視窗風格的代碼來通路‘擴充’風格,為了關閉使幫助按鈕顯示的位,在回調函數響應BFFM_INITIALIZED消息時中所必須這樣做:

DWORD dwStyle = GetWindowLong(hwnd, GWL_EXSTYLE);

SetWindowLong(hwnd, GWL_EXSTYLE, dwStyle & ~WS_EX_CONTEXTHELP);

首先,取得目前擴充風格(用GWL_EXSTYLE 代替 GWL_STYLE),然後關閉指定位,最後回存這個風格值。

給狀态文字加3D邊框

         做這個比前一個稍微複雜一點,并且要求多行代碼。然而你應該清楚,現在所做的并不能保證你的代碼在所有存在的和未來的Windows版本中都能正常工作。它僅僅能工作在你已經成功地測試了這個功能的地方。

         現在,我們想要繪制一個帶有3D邊框的狀态标簽,就象狀态條那樣。BIF_STATUSTEXT标志可能有點誤導—它不是象所期望的那樣在視窗的底部加一個狀态條,而是在樹觀察上方和标題之間加一個靜态标簽。這個标簽視窗有一個控件ID,我們可以通過Spy++得知:

Windows Shell程式設計-第五章.浏覽檔案夾

當你知道了這個控件的ID之後,一旦回調函數進入到對話框代碼之中,獲得任何子視窗的Handle就象調用下述代碼那樣容易:

HWND hwndChild = GetDlgItem(hDlg, controlID);

在上圖中你可以看到,我們感興趣的标簽有ID号0x3743,是以:

HWND hwndLabel = GetDlgItem(hwnd, 0x3743);

dwStyle = GetWindowLong(hwndLabel, GWL_EXSTYLE);

SetWindowLong(hwndLabel, GWL_EXSTYLE, dwStyle | WS_EX_STATICEDGE);

SetWindowPos(hwndLabel, NULL, 0, 0, 0, 0,

SWP_NOSIZE | SWP_NOMOVE | SWP_DRAWFRAME);

上述代碼加了一個細邊框到視窗中,外觀如下圖所示:

Windows Shell程式設計-第五章.浏覽檔案夾

注意,為了看到這個變化,你需要強迫視窗重畫它的非客戶區域,這就是SetWindowPos()函數出現的原因。再有就是,所有這些操作均在回調函數響應BFFM_INITIALIZED消息時完成。

         前面我已經警告過,這段代碼潛在的風險,現在,在所有Win32 平台上都能很好地工作,但是有一天微軟決定改變這個ID時,将會怎樣?一個好的方法可能是:

HWND hwndLabel = GetDlgItem(hwnd, 0x3743);

// 檢查是否為一可用的視窗

if(IsWindow(hwndLabel))

{

// 現在檢查體是否為一個'static'視窗類

TCHAR szClass[MAX_PATH] = {0];

GetClassName(hwndLabel, szClass, MAX_PATH);

if(lstrcmpi(szClass, __TEXT("static")))

return;

}

else

return;

我們對GetDlgItem()函數傳回的Handle執行了兩個檢查,頭一個是使用IsWindow()來檢查是否為有效的視窗,第二個是驗證這個視窗是不是一個标簽—一個‘static’類視窗。如果其中有一個檢查失敗,則退出,以避免非法操作。

改變對話框标題

         比加3D邊框更有用的是改變對話框視窗的标題—使用新字元串調用SetWindowText(),即,在響應BFFM_INITIALIZED消息時,執行下面代碼:

SetWindowText(hwnd, szNewCaption);

移動對話框視窗

         另一個在響應BFFM_INITIALIZED消息的初始化中可以做的是定位視窗到指定位置。典型地,可以移動對話框到螢幕中心:

RECT rc;

GetClientRect(hwnd, &rc);

SetWindowPos(hwnd, NULL,

(GetSystemMetrics(SM_CXSCREEN) - (rc.right - rc.left)) / 2,

(GetSystemMetrics(SM_CYSCREEN) - (rc.bottom - rc.top)) / 2,

0, 0, SWP_NOZORDER | SWP_NOSIZE);

變動狀态标簽

         狀态标簽的典型用途是顯示目前選中的檔案或檔案夾名,就象在前面圖中顯示的那樣。其實作方法就是響應BFFM_SELCHANGED消息:

TCHAR szText[MAX_PATH] = {0};

SHGetPathFromIDList(reinterpret_cast<LPITEMIDLIST>(lParam), szText);

SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0, reinterpret_cast<LPARAM>(szText));

在接收到這個消息的時候,lParam變量指向一個新選中檔案夾或檔案的PIDL,說明檔案或檔案夾是存在的,是以可以調用SHGetPathFromIDList()函數獲得可顯示的路徑名,注意,不是所有檔案夾映射到實體目錄—例如‘我的計算機’,是一個沒有實際目錄的檔案夾。如果使用‘我的計算機’的PIDL調用SHGetPathFromIDList()函數,可能會獲得一個NULL字元串。

    從SHGetPathFromIDList()中恢複的字元串可以使用BFFM_SETSTATUSTEXT消息發送到狀态視窗。

允許手動編輯

         自綁定到IE4.0的Shell 4.71版開始,這個對話框的使用者界面就加入了一個編輯框,而且不需要借助于回調方式就可以操作。你隻要在調用SHBrowseForFolder()函數時簡單地設定BIF_EDITBOX标志即可。結果顯示如下:

Windows Shell程式設計-第五章.浏覽檔案夾

這個編輯框使你能夠用輸入檔案夾的名字來選擇它們。當你點選‘OK’時,函數将确認你的輸入有效。如果編輯框中包含了一個檔案夾的全路徑名,或目前選擇的檔案夾名,則它的内容是正确的,如上圖所示。如果BIF_VALIDATE标志設定,并且SHBrowseForFolder()函數發現編輯框的内容是不正确的,則它用BFFM_VALIDATEFAILED消息喚醒回調函數,在編輯框中的字元串經由lParam變量傳遞給回調函數。經由BROWSEINFO結構的lParam成員傳遞的任何使用者資料,作為回調函時的第四個參數傳遞。是以,如果需要通過輸入選擇檔案夾,絕對必須輸入全路徑名。

         下面列出的代碼例子說明我們目前所看到的全部情況,後面章節中将給出整個應用的例程。

int CALLBACK BrowseCallbackProc(

HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM dwData)

{

switch(uMsg)

{

case BFFM_INITIALIZED:

{

// 删除标題中的?

DWORD dwStyle = GetWindowLong(hwnd, GWL_EXSTYLE);

SetWindowLong(hwnd, GWL_EXSTYLE, dwStyle & ~WS_EX_CONTEXTHELP);

// 給狀态文字加3D邊框

HWND hwndLabel = GetDlgItem(hwnd, 0x3743);

// 檢查是否為有效的視窗

if(IsWindow(hwndLabel))

{

// 檢查是否為'static'類視窗

TCHAR szClass[MAX_PATH] = {0};

GetClassName(hwndLabel, szClass, MAX_PATH);

if(lstrcmpi(szClass, __TEXT("static")))

break;

}

else

break;

dwStyle = GetWindowLong(hwndLabel, GWL_EXSTYLE);

SetWindowLong(hwndLabel, GWL_EXSTYLE, dwStyle | WS_EX_STATICEDGE);

SetWindowPos(hwndLabel, NULL, 0, 0, 0, 0,

SWP_NOSIZE | SWP_NOMOVE | SWP_DRAWFRAME);

}

break;

case BFFM_SELCHANGED:

{

TCHAR szText[MAX_PATH] = {0};

SHGetPathFromIDList(reinterpret_cast<LPITEMIDLIST>(lParam),

szText);

SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0,

reinterpret_cast<LPARAM>(szText));

}

break;

case BFFM_VALIDATEFAILED:

Msg("/"%s/" is a wrong path name.",

reinterpret_cast<LPTSTR>(lParam));

return 1;

}

return 0;

}

指定初始檔案夾

         SHBrowseForFolder()函數設計時的一個缺陷是沒有一個友善的方法來指定開始浏覽的初始目錄。你可以指定顯示層的根,就是這樣,如果想要使用目錄而不是檔案夾,也不簡單。為了在代碼中設定初始選中的檔案夾,我們必須借助于回調函數,特别是,必須探索BFFM_SETSELECTION消息,和請求函數移動焦點到一個特殊的檔案夾。實作的最好方式是在響應BFFM_INITIALIZED通知消息的地方,下面就是實作的代碼:

int CALLBACK BrowseCallbackProc(

HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM dwData)

{

switch(uMsg)

{

case BFFM_INITIALIZED:

{

...

SendMessage(hwnd, BFFM_SETSELECTION, TRUE, dwData);

}

break;

...

}

return 0;

}

BFFM_SETSELECTION消息需要知道lParam變量是一個PIDL還是一個路徑名,在上面代碼中因為設定(第三個參數)為TRUE,是以dwData指向一個路徑名,若dwData指向PIDL,則第三個參數應該設定為FALSE。

指定根節點

         前面已經暗示,SHBrowseForFolder()函數允許指定桌面層上的哪一個節點作為根節點。換句話說,你可以選擇想要浏覽的探測器觀察子樹。能這樣做的參數就是BROWSEINFO結構的pidlRoot成員。如果這個成員設定為NULL,樹觀察以桌面作為根。下面圖中顯示浏覽對話框以‘列印機’作為根節點,并且設定了BIF_BROWSEINCLUDEFILES标志:

Windows Shell程式設計-第五章.浏覽檔案夾

附帶地,這個例子還說明,BIF_BROWSEINCLUDEFILES的作用。代碼如下:

LPITEMIDLIST pidl = NULL;

BROWSEINFO bi;

ZeroMemory(&bi, sizeof(BROWSEINFO));

bi.lpszTitle = __TEXT("Choose a printer:");

SHGetSpecialFolderLocation(NULL, CSIDL_PRINTERS, &pidl);

bi.pidlRoot = pidl;

bi.ulFlags = BIF_BROWSEINCLUDEFILES;

SHBrowseForFolder(&bi);

如果核對一下BROWSEINFO結構的聲明,你将看到,pidlRoot成員應該有LPCITEMIDLIST類型—即,PIDL。在上面代碼段中,通過在函數SHGetSpecialFolderLocation()第二個參數中傳遞CSIDL_PRINTERS,我們已經取得了這個值,這一點将在後面繼續讨論。現在,概括地講,你可以指定顯示樹的根節點,但是需要提供一個PIDL。

使用目錄作為根

         如果我們的目标正好就是浏覽某個系統檔案夾,如‘列印機’或‘字型’或‘Favorites’,沒有問題—使用上面的代碼段,隻需把CSIDL_PRINTERS換成想要的檔案夾ID即可。如果想要把普通的目錄作為樹觀察的根,事情就有點嚴重了。

    對于特殊檔案夾ID,翻看SHGetSpecialFolderLocation()函數的資料或研究shlobj.h源碼就可以找到某些資料沒有說明的IDs。

轉換路徑名到PIDL

         沒有别的辦法,你隻有轉換路徑名到PIDL。現在,你可能希望有一個Shell的API函數做這個轉換,不幸的是沒有這樣的函數。然而,有一種方法能夠轉換路徑名到PIDL,這需要兩個步驟:

                   獲得指向IShellFolder接口的指針

        調用它的ParseDisplayName()的方法

ParseDisplayName()方法确實能實作你所要求的轉換:它接受路徑名,然後把它轉換成PIDL,問題是我們怎樣才能獲得指向IShellFolder接口的指針。在你寫一個命名空間擴充的時候,IShellFolder接口是一個需要實作的接口,并且,探測器也使用這個接口一起工作,請求繪制和枚舉其内容。一個指向IShellFolder接口的指針由SHGetDesktopFolder()函數傳回—精确地講,它傳回一個桌面檔案夾的IShellFolder接口。就我們考慮的情況而言,我們隻是需要一個提供實際實作ParseDisplayName()功能的對象指針,因而SHGetDesktopFolder()函數傳回的是可用的。下面是一個新Shell函數的代碼,它接受路徑名和傳回對應的PIDL,以微軟命名習慣,我們稱之為SHPathToPidl():

HRESULT SHPathToPidl(LPCTSTR szPath, LPITEMIDLIST* ppidl)

{

LPSHELLFOLDER pShellFolder = NULL;

OLECHAR wszPath[MAX_PATH] = {0};

ULONG nCharsParsed = 0;

// 擷取IShellFolder接口指針

HRESULT hr = SHGetDesktopFolder(&pShellFolder);

if(FAILED(hr))

return hr;

// 轉換路徑名到Unicode

MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszPath, MAX_PATH);

// 調用ParseDisplayName()函數

hr = pShellFolder->ParseDisplayName(

NULL, NULL, wszPath, &nCharsParsed, ppidl, NULL);

// 清理

pShellFolder->Release();

return hr;

}

ParseDisplayName()函數的原形如下:

HRESULT ParseDisplayName(HWND hwndOwner,

LPBC pbcReserved,

LPOLESTR lpszDisplayName,

ULONG* pchEaten,

LPITEMIDLIST* ppidl,

ULONG* pdwAttributes);

第一個變量是用作函數可能需要顯示消息框的父視窗的Handle。第二個,pbcReserved沒有使用,必須設定為NULL。頭一個有意義的變量是lpszDisplayName,表示一個要求轉換的Unicode格式的名字串,pchEaten是一個包含實際傳遞字元數的緩沖,而pdwAttributes(如果不空NULL),則包含由lpszDisplayName所指定檔案夾的屬性,這些屬性都是SHGAO_字首常量屬性。如果不注意,可能會傳遞NULL屬性。最後是ppidl變量,新生成的PIDL的傳回緩沖。一旦對指定的路徑成功地生成了PIDL,你就能夠限制使用者的浏覽到指定的子樹,而不能繼續向上,例如:

Windows Shell程式設計-第五章.浏覽檔案夾

這個圖顯示C:/Program Files作為根檔案夾的情形。

總結例程

         到目前為止,我們孤立地讨論了SHBrowseForFolder()函數,并且總是給出解決特殊問題的代碼段,現在,我們構造一個完整的應用來總結上面所有我們看到的特性。

Windows Shell程式設計-第五章.浏覽檔案夾

圖中的對話框是測試SHBrowseForFolder()函數特征所使用過的,稱之為SHBrowse。通過選擇路徑名(檔案夾編輯框)或PIDL(PIDL清單框)指定根檔案夾—使用PIDL複選框确定是哪一種路徑。可以在标題編輯框中設定對話框标題,以及幾個與SHBrowseForFolder()函數标志大緻比對的複選框,結果顯示在下面的區域,顯示名,路徑名和檔案夾圖示。增加的第一段代碼在OnInitDialog()函數中,以便使用特殊檔案夾的名字設定PIDL下拉清單框。

void OnInitDialog(HWND hDlg)

{

// 設定圖示(T/F 大/小圖示)

SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));

SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));

// 填寫下拉清單框

HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);

int i = ComboBox_AddString(hwndCbo, "Control Panel");

ComboBox_SetItemData(hwndCbo, i, CSIDL_CONTROLS);

i = ComboBox_AddString(hwndCbo, "Favorites");

ComboBox_SetItemData(hwndCbo, i, CSIDL_FAVORITES);

i = ComboBox_AddString(hwndCbo, "Printers");

ComboBox_SetItemData(hwndCbo, i, CSIDL_PRINTERS);

i = ComboBox_AddString(hwndCbo, "Fonts");

ComboBox_SetItemData(hwndCbo, i, CSIDL_FONTS);

i = ComboBox_AddString(hwndCbo, "SendTo");

ComboBox_SetItemData(hwndCbo, i, CSIDL_SENDTO);

ComboBox_SetCurSel(hwndCbo, 0);

}

此時你可以從這個清單中選擇‘發送到’(SendTo)檔案夾,這依賴于你的其它選擇設定。如圖:

Windows Shell程式設計-第五章.浏覽檔案夾

當然,這個對話框在你自己的計算機上可能稍有不同,這是因為‘SendTo’目錄可能有不同的快捷方式。在此選擇‘OutLook Express’,結果如下:

Windows Shell程式設計-第五章.浏覽檔案夾

整個項目是可用的,代碼包含了BrowseCallbackProc(),SHGetSystemIcon()和SHPathToPidl()函數。編譯之前一定要記住#include shlobj.h 和resource.h。這些函數在點選OK按鈕時被執行。

void OnOK(HWND hDlg)

{

BROWSEINFO bi;

TCHAR szTitle[MAX_PATH] = {0};

TCHAR szPath[MAX_PATH] = {0};

TCHAR szDisplay[MAX_PATH] = {0};

LPITEMIDLIST pidl = NULL;

LPMALLOC pMalloc = NULL;

// 準備

ZeroMemory(&bi, sizeof(BROWSEINFO));

bi.hwndOwner = hDlg;

// 标題和顯示名

GetDlgItemText(hDlg, IDC_TITLE, szTitle, MAX_PATH);

bi.lpszTitle = szTitle;

bi.pszDisplayName = szDisplay;

//初始目錄

if(IsDlgButtonChecked(hDlg, IDC_USEPIDL))

{

HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);

int i = ComboBox_GetCurSel(hwndCbo);

int nFolder = ComboBox_GetItemData(hwndCbo, i);

SHGetSpecialFolderLocation(NULL, nFolder, &pidl);

bi.pidlRoot = pidl;

}else{

// 轉換路徑名到 PIDL

GetDlgItemText(hDlg, IDC_FOLDER, szPath, MAX_PATH);

if(lstrlen(szPath) == 0)

GetCurrentDirectory(MAX_PATH, szPath);

SHPathToPidl(szPath, &pidl);

bi.pidlRoot = pidl;

}

// 采集标志

UINT uiFlags = 0;

if(IsDlgButtonChecked(hDlg, IDC_NOBELOW))

uiFlags |= BIF_DONTGOBELOWDOMAIN;

if(IsDlgButtonChecked(hDlg, IDC_ONLYDIRS))

uiFlags |= BIF_RETURNONLYFSDIRS;

if(IsDlgButtonChecked(hDlg, IDC_INCLUDEFILES))

uiFlags |= BIF_BROWSEINCLUDEFILES;

if(IsDlgButtonChecked(hDlg, IDC_EDITBOX))

uiFlags |= BIF_EDITBOX | BIF_VALIDATE;

if(IsDlgButtonChecked(hDlg, IDC_STATUS))

uiFlags |= BIF_STATUSTEXT;

if(IsDlgButtonChecked(hDlg, IDC_COMPUTER))

uiFlags |= BIF_BROWSEFORCOMPUTER;

bi.ulFlags = uiFlags;

// 設定回調

bi.lpfn = BrowseCallbackProc;

bi.lParam = 0;

// 調用函數

LPITEMIDLIST pidlFolder = SHBrowseForFolder(&bi);

if(pidlFolder == NULL)

return;

// 顯示結果...

// 顯示顯示名

SetDlgItemText(hDlg, IDC_DISPLAYNAME, bi.pszDisplayName);

// 顯示路徑名

SHGetPathFromIDList(pidlFolder, szPath);

SetDlgItemText(hDlg, IDC_PATHNAME, szPath);

// 顯示檔案夾圖示

HICON hIcon = SHGetSystemIcon(bi.iImage);

SendDlgItemMessage(

hDlg, IDI_ICON, STM_SETICON, reinterpret_cast<WPARAM>(hIcon), 0);

// 釋放

SHGetMalloc(&pMalloc);

pMalloc->Free(pidl);

pMalloc->Free(pidlFolder);

pMalloc->Release();

}

上面函數的工作方式對你來講是很顯然的—除了,最後面的一段。為了進一步解釋它,我們需要更深地了解關于PIDL的知識。

有點變态的PIDL

         在第二章中我們檢測了PIDLs,在這裡,我們得到了它的特殊應用:使用PIDL浏覽檔案夾的内容,無論這個内容是什麼。每一個Windows Shell 的元素都有它自己的PIDL并且包含在某種檔案夾中,因而,對于每一個元素,都有一段代碼來處理檔案夾和依據檔案夾自有的規則和需求提供PIDL。也就是說,我們從不能設定PIDL結構或它所組合成的資料,而是必須使用通用接口來處理它。例如,如果希望探索SHITEMID結構鍊,你就應該在每一步檢查下一個塊的長度。就像你已經看到過的那樣,一個ITEMIDLIST—或PIDL—是由一個或多個連續配置設定的SHITEMID組成的,這個鍊在cb字段為0的元素上終止。下面是從MSDN上摘錄的函數,它可以說明怎樣周遊這個清單的項。這與普通的清單操作沒有太大的差别。

LPITEMIDLIST GetNextItemID(LPITEMIDLIST pidl)

{

// 取得制定項的尺寸

int cb = pidl->mkid.cb;

// 尺寸為零則清單終止

if(cb == 0)

return NULL;

// 加cb到pidl (生成位元組增量)

pidl = (LPITEMIDLIST)(((LPBYTE)pidl) + cb);

// 如果NULL終止則傳回NULL,否則傳回一個pidl

return (pidl->mkid.cb == 0) ? NULL : pidl;

}

你不可以設定PIDL的格式,對于一個檔案夾,一種方法可能工作的很好,對于另一個可能會失敗。例如,為了保證兩個項是相同的,你必須通過IShellFolder::CompareIDs()方法請求檔案夾自己來比較它們。

釋放PIDL

         在進一步讨論之前,有必要解釋一下上面例子中的最後一段代碼。在檔案夾建立PIDLs時,通常必須由其它子產品銷毀,這就是我們在OnOK()函數最後所做的。辨別符清單的記憶體從Shell應用的配置設定器上取得,向第二章中所見,我們可以調用SHGetMalloc()函數獲得指向配置設定器的指針。一般,調用順序如下:

LPMALLOC pMalloc;

SHGetMalloc(&pMalloc); // 取得IMalloc 接口

pMalloc->Free(pidl); // 釋放辨別符清單

pMalloc->Release(); // 釋放IMalloc 接口

怎樣使用PIDL

         回到我們的題目當中,使PIDLs有某些實際的用途,這裡有兩個主要的目标,頭一個,我們想要能夠枚舉任何檔案夾的内容,第二個是希望重複探測器在Shell在4.71和更高版本中所支援的特征。為了展示這個想法,下面是探測器在位址欄輸入‘列印機’後,鍵入‘回車’截圖:

Windows Shell程式設計-第五章.浏覽檔案夾

探測器允許使用‘列印機’作為通常意義的檔案夾名。換言之,它模糊了實體檔案夾和虛拟檔案夾的差異,精确地講,‘列印機’是一個内容為有效列印機裝置的虛拟檔案夾的顯示名。在這個例子中我們建立一個示範程式稱之為Pidl,其使用者界面如下:

Windows Shell程式設計-第五章.浏覽檔案夾

‘搜尋路徑’按鈕接受編輯框的内容并努力查找具有這個名字的檔案夾。編輯框中的串就是一個檔案夾的顯示名(記住,路徑名也是顯示名)。如果成功,應用将在清單觀察中顯示所有在這個檔案夾中找到的檔案對象。另一方面,‘顯示PIDL内容’按鈕将在清單觀察中枚舉特殊檔案夾中找到的所有檔案對象,特殊檔案夾在下拉清單中選擇。

通過顯示名搜尋

         讓我們從點選‘搜尋路徑’按鈕所執行的代碼開始,當然這兩個新按鈕都需要APP_DlgProc()過程來處理,是以在這裡添加代碼如下:

case WM_COMMAND:

switch(wParam)

{

case IDC_SEARCHPATH:

DoSearchPath(hDlg);

return FALSE;

case IDC_PIDLCONTENT:

DoEnumeratePidl(hDlg);

return FALSE;

case IDCANCEL:

EndDialog(hDlg, FALSE);

return FALSE;

}

break;

這裡所涉及到的第一個函數是DoSearchPath(),它從‘檔案夾名’編輯框中讀出你鍵入的名字,把它作為搜尋路徑名,如果它确實是一個路徑名,所有操作都正常進行,但是如果它是一個檔案夾的顯示名,将會有什麼事情發生呢?我們希望函數能夠處理如C:/和(C:)等的串,這個實作将能夠正确處理所有路徑名和與桌面關聯的子檔案夾的顯示名,或‘我的計算機’等虛拟檔案夾。

注意,正常地,驅動器的顯示名由括弧中驅動器字元加标号給出,例如:Ms-Dos_6(C:)。如果沒有标号,則認為有一個前導空格在(C:)中。

DoSearchPath()一開始就枚舉桌面檔案夾的内容:

void DoSearchPath(HWND hDlg)

{

LPITEMIDLIST pidl = NULL;

LPSHELLFOLDER pFolder = NULL;

LPSHELLFOLDER pSubFolder = NULL;

// 取得記憶體配置設定器

LPMALLOC pMalloc = NULL;                                    

SHGetMalloc(&pMalloc);

// 取得搜尋名

TCHAR szName[MAX_PATH] = {0};

GetDlgItemText(hDlg, IDC_FOLDER, szName, MAX_PATH);

// 取得卓面的IShellFolder接口

SHGetDesktopFolder(&pFolder);

// 試圖在桌面上找到一個比對

int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

int rc = SHEnumFolderContent(

pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);

SHEnumFolderContent()是使用者定義的函數,他接受一個檔案夾的PIDL和一個回調函數作為輸入,然後枚舉這個檔案夾的所有項,并傳遞這些項到這個函數作進一步的處理。後面我們将進一步讨論它。為了了解它在這裡的用途,你隻需要知道,如果沒有指定回調函數,它傳回所找到的項目數:

int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

否則,它傳回實際處理的項目數。這兩個值必然是不同的,因為回調函數可以在本身選中的點上停止枚舉。例如SearchText()函數,當它找到了它正在尋找的名字時,SHEnumFolderContent()停止了。

    SHEnumFolderContent()函數開始搜尋,檢查我們在編輯框中鍵入的名字是否對應桌面下一個檔案夾的顯示名。這就是上面代碼終止後,rc 和 iNumOfItems不等的情況。如果它們相等,我們就在‘我的計算機’節點上開始一個新的搜尋:

// 如果沒找到,在‘我的計算機’上再試

if(rc == iNumOfItems)

{

// 綁定到‘我的計算機’

LPITEMIDLIST pidlMyComp;

SHGetSpecialFolderLocation(NULL, CSIDL_DRIVES, &pidlMyComp);

pFolder->BindToObject(pidlMyComp, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

//釋放桌面檔案夾指針

pFolder->Release();

pMalloc->Free(pidlMyComp);

pFolder = pSubFolder;

//掃描‘我的計算機’

iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

rc = SHEnumFolderContent(

pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);

在重複調用SHEnumFolderContent()函數工作于‘我的計算機’檔案夾上之前,我們需要為之獲得IShellFolder接口指針,在這一點上我們有的隻是桌面的IShellFolder接口,然而,我們可以通過接口的BindToObject()方法獲得想要的接口。這個操作使你能綁定到子檔案夾的IShellFolder接口,是以,你可以同樣使用這個方法作用于PIDL。

HRESULT IShellFolder::BindToObject(

LPCITEMIDLIST pidl, // 我們想要的檔案夾的PIDL

LPBC pbcReserved, // 保留,必須為空NULL

REFIID riid, // 必須是IID_IShellFolder

LPVOID* ppvOut // 接收IShellFolder 的指針

);

如果既沒在桌面上也沒在‘我的計算機’上找到指定的顯示名,則我們所取得的是一個快捷方式,而且我們确實不能确定它的位置。盡管如此,也不要認為這是一個系統限制—在檔案夾上使用遞歸搜尋來确定名字的位置是很有可能的。這個方法略述如下:

                   枚舉桌面檔案夾的内容,就象上面所做的。

                   對每一個找到的檔案夾(不僅是‘我的計算機’)重複這個搜尋過程

然而,完全遞歸的搜尋可能導緻試圖通過名字查找一個不唯一的檔案夾—這是很有可能的。當然可能有兩個檔案夾具有相同的顯示名MyDir,一個在c:/,另一個在d:/,上面的算法将總是停止在頭一個檔案夾出現的地方。

         一個較好的方法是接受和分析全品質檔案夾名,比如:

My Computer/ (c:)/Windows

Control Panel/Add New Hardware

這樣做僅僅需要很少的額外代碼來分析檔案夾名,并且從搜尋桌面上的第一個項開始,前一步完成到達檔案夾的下一個項,等等。上面所看到的代碼可以稍微加推廣,和封裝在一個循環中。

         回想一下,這确實與在檔案系統中搜尋沒有什麼不同,正好就是使用FindFirstFile()和 FindNextFile()來枚舉目錄的内容,隻是使用了由檔案夾對象的COM接口暴露的方法而已。

         在完成代碼之前,注意,鍵入的顯示名是一個完整的路徑名的情況,如c:/。在輸出一個消息框之前處理這種情況是有價值的—正象我們需要轉換這個名字為PIDL格式一樣,看一下什麼情況發生了,如果沒有錯誤,則一個路徑名被接受了。

if(rc == iNumOfItems)

{

// 做最後的努力,它是一個路徑名

HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);

if(FAILED(hr))

{

Msg("/"%s/" not found under Desktop or My Computer.", szName);

pMalloc->Free(pidl);

pFolder->Release();

// 調用輔助函數重新整理UI

ClearUI(hDlg);

return;

}

}

}

最後,如果函數在這一點之前沒有傳回,我們知道,已經有了一個可用于輸出檔案夾圖示的PIDL,也就是說,在編輯框中有一個輸入串作為上面源碼中的szName被引用。我們就用那個名字辨別檔案夾對象并獲得它的PIDL。現在,要枚舉這個檔案夾的内容,我們需要獲得它的IShellFolder接口和把它傳遞給SHEnumFolderContent()函數。

    因而,‘搜尋路徑’按鈕處理程式的結尾代碼有如下形式:

// 如果到達這裡,則:

// pidl 指向我們需要綁定的檔案夾以枚舉它的内容

// pFolder 指向pidl父檔案夾的IShellFolder

// 綁定到我們正在搜尋的子檔案夾

// pFolder 可以指向桌面的或‘我的計算機’的IShellFolder

pFolder->BindToObject(pidl, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

// 重新整理UI (清空清單觀察和圖像清單等)

ClearUI(hDlg);

// 枚舉檔案夾内容到清單觀察

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

SHEnumFolderContent(pSubFolder, ShowFolderContent,

reinterpret_cast<DWORD>(hwndListView), NULL);

// 清理

pFolder->Release();

pSubFolder->Release();

pMalloc->Free(pidl);

pMalloc->Release();

return;

}

轉換路徑名到PIDLs(續)

         檢視上面的代碼,你可能已經注意到,我使用了SHPathToPidlEx()函數來轉換路徑名到PIDL。在這一章的開始,我們開發了有同樣目的的SHPathToPidl()輔助函數—它使用IShellFolder接口的ParseDisplayName()方法。SHPathToPidl()函數的代碼濃縮到這一點上,它取得相對于桌面的PIDL—即,層次的根是

SHGetDesktopFolder(&pFolder);

pFolder->ParseDisplayName(NULL, NULL, wszPath, &n, ppidl, NULL);

不幸的是這個PIDL是相對于提供IShellFolder接口檔案夾的,是桌面。新情況是我們需要相對于操作檔案夾之父檔案夾的PIDL。理由是,當我們使用BindToObject()方法來獲得一個子檔案夾的IShellFolder時,要求傳遞一個PIDL,它是與我們調用BindToObject()位于相同位置檔案夾的PIDL。

    此後,我們還需要在獲得IShellFolder接口指針和ParseDisplayName()函數之間添加幾步,這幾步是要保證用于ParseDisplayName()調用的IShellFolder接口确實是我們想要的檔案夾的接口。

代碼如下:

HRESULT SHPathToPidlEx(

LPCTSTR szPath, LPITEMIDLIST* ppidl, LPSHELLFOLDER pFolder)

{

OLECHAR wszPath[MAX_PATH] = {0};

ULONG nCharsParsed = 0;

LPSHELLFOLDER pShellFolder = NULL;

BOOL bFreeOnExit = FALSE;

MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszPath, MAX_PATH);

// 預設使用桌面的IShellFolder

if(pFolder == NULL)

{

SHGetDesktopFolder(&pShellFolder);

bFreeOnExit = TRUE;

}

else

pShellFolder = pFolder;

HRESULT hr = pShellFolder->ParseDisplayName(

NULL, NULL, wszPath, &nCharsParsed, ppidl, NULL);

if(bFreeOnExit)

pShellFolder->Release();

return hr;

}

這個函數比SHPathToPidl()更一般,而且它也要求傳遞PIDL相對的檔案夾。如果傳遞一個NULL,而不是一個IShellFolder指針,則桌面的IShellFolder接口被使用,然後被釋放。在這個例子中調用轉換函數的代碼是:

HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);

試着傳遞一個NULL而不是确定的路徑名給pFolder,進行搜尋,将會看到:無論如何,你将總是枚舉‘桌面’檔案夾的内容。

清除使用者界面

         作為SHEnumFolderContent()細節的一部分,在這個代碼中我們所看到的最簡單的輔助函數是ClearUI():

void ClearUI(HWND hDlg)

{

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

ListView_DeleteAllItems(hwndListView);

ImageList_RemoveAll(g_himl);

SetDlgItemText(hDlg, IDC_FOUND, __TEXT("0 item(s) found."));

}

這正是重置應用的對話框,從觀察清單中删除所有項,以及清空由SHEnumFolderContent()函數建立的圖像清單,最後一項任務由HIMAGELIST類型的全稱變量g_himl完成,它在WinMain()中被初始化為0。

建構枚舉器函數

         關于枚舉給定檔案夾内容這方面仍然有大量的問題需要考慮,比如,它是一個實體目錄還是一個如‘列印機’的虛拟檔案夾等。我們所看到的源碼給出一個重要的函數SHEnumFolderContent(),這是一個負責查詢檔案夾和逐個枚舉其内容的函數。

    有些檔案夾其内容是檔案的集合,還有些檔案夾其可見内容可以是單個檔案的記錄或某種硬體裝置。一般而言,僅僅是檔案夾才确切地知道它的内容是什麼。對探測器或程式而言,沒有任何保險的方法不需要查詢檔案夾就能枚舉它所包含的項。這并不奇怪,因為這種通訊是基于COM接口的。

         在程式中,SHEnumFolderContent()詢問一個檔案夾的内容,并傳輸它找到的每一個項的名字到另外的函數做進一步的處理。你已經看到了這兩個函數:SearchText()和ShowFolderContent()。然而要了解它們的适當作用,首先要研究項目枚舉是怎樣發生的。

讀檔案夾的内容

         關聯于‘搜尋路徑’(和顯示PIDLs内容)按鈕的代碼,其目的在于讀出檔案夾的内容。為了允許枚舉其中的項,檔案夾實作了IEnumIDList接口,這個接口暴露了四個函數:Next(), Skip(), Reset()和 Clone(),使之可以在給定的集合中前後移動。它們的原型為:

HRESULT IEnumIDList::Next(ULONG celt,

LPITEMIDLIST* rgelt,

ULONG* pceltFetched);

頭一個變量是請求的項目數,第二個是一個PIDLs數組指針,第三個則是傳回的實際拷貝的項目數。IEnumIDList接口本身負責配置設定儲存PIDL資料的記憶體。

         要想了解指定檔案夾的内容,需要設計一段代碼,要記住是通過擷取指向IEnumIDList接口的指針和IShellFolder接口所暴露的方法EnumObjects()實際完成這個操作的。這個方法的原型如下:

HRESULT IShellFolder::EnumObjects(HWND hwndOwner, //一個視窗Handle

DWORD grfFlags, //一個标志集

LPENUMIDLIST* ppenumIDList //接收IEnumIDList的指針

);

這個方法的第二個參數允許你規定枚舉項目的類型。它取如下定義的枚舉類型組合值:

typedef enum tagSHCONTF

{

SHCONTF_FOLDERS = 32,

SHCONTF_NONFOLDERS = 64,

SHCONTF_INCLUDEHIDDEN = 128,

} SHCONTF;

就如助記名所述的一樣:你可以決定枚舉檔案夾、非檔案夾對象,甚至是隐藏對象。

         以後在寫一個命名空間擴充得時候,詳細讨論這些接口是非常必要的。現在,我們建議你花點時間看一下VC++的幫助檔案,以澄清這些方法名和原型。

LPENUMIDLIST pEnumIDList = NULL;

LPITEMIDLIST pItem = NULL;

ULONG ulFetched = 0;

pFolder->EnumObjects(

NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);

while(pEnumIDList->Next(1, &pItem, &ulFetched) == NOERROR)

{

...

}

上面這段代碼描述了觸發枚舉檔案夾項函數的過程。每次循環條件成立,pItem指向一個PIDL的單項。獲得這個資料後有兩件事情需要處理:它的顯示名和可能的圖示。

取得項的顯示名

         即使有了PIDL,取得項目的顯示名也不是一件容易的事。盡管有IShellFolder::GetDisplayNameOf()函數,仍有一些工作要做,問題在于這個方法不提供正常的ANSI或Unicode格式。相反,它傳回的是一個STRRET結構的指針。STRRET結構定義如下:

typedef struct _STRRET

{

UINT uType;

union{

LPWSTR pOleStr;

LPSTR pStr; // Unused

UINT uOffset;

char cStr[MAX_PATH];

} DUMMYUNIONNAME;

} STRRET, *LPSTRRET;

正像你所看到的,這個結構用一個标志表示格式,這個标志說明了其後字元串的類型。串可以是Unicode串(pOleStr),ANSI串(cStr),或甚至是一個串的位址偏移(uOffset)。也就是說不管初始串是什麼格式的,你都需要自己寫展開例程處理傳回串。這裡給出一個樣例:

void StrretToString(LPITEMIDLIST pidl, LPSTRRET pStr, LPSTR pszBuf)

{

lstrcpy(pszBuf, "");

switch(pStr->uType)

{

case STRRET_WSTR: // Unicode 串

WideCharToMultiByte(

CP_ACP, 0, pStr->pOleStr, -1, pszBuf, MAX_PATH, NULL, NULL);

break;

case STRRET_OFFSET: // 位址偏移

lstrcpy(pszBuf, reinterpret_cast<LPSTR>(pidl) + pStr->uOffset);

break;

case STRRET_CSTR: // ANSI 串

lstrcpy(pszBuf, pStr->cStr);

break;

}

}

StrretToString()函數接收一個PIDL和一個STRRET結構,經由第三個變量傳回一個LPSTR。附帶地上面代碼還顯示了uType的合法值。回到我們主要讨論的話題,GetDisplayNameOf()函數的原型是:

HRESULT IShellFolder::GetDisplayNameOf(LPCITEMIDLIST pidl,

DWORD uFlags,

LPSTRRET lpName);

這裡uFlags标志來自SHGNO枚舉類型:

typedef enum tagSHGDN

{

SHGDN_NORMAL = 0,

SHGDN_INFOLDER = 1,

SHGDN_INCLUDE_NONFILESYS = 0x2000,

SHGDN_FORADDRESSBAR = 0x4000,

SHGDN_FORPARSING = 0x8000,

} SHGNO;

資料中對這些标志的描述是足夠清晰的。是以,你可以建構你所期望的具有最終行為的函數。然而,無論我怎樣設定标志,所有樣例程式都是以同樣的方式工作。坦白地說,我也不知道哪兒除了問題。是以我建議總是使用0作為這個參數的值。

STRRET sName;

CHAR szBuf[MAX_PATH] = {0};

pFolder->GetDisplayNameOf(pItem, 0, &sName);

StrretToString(pItem, &sName, szBuf);

這個代碼段以可閱讀格式顯示項目名。再次提醒注意,對于檔案型檔案夾以及Fonts, Favorites, Printers, Control Panel等特殊檔案夾這是正确的。也就是說,我們能夠列出所有‘控制台’中的小程式。

         本身,我們沒有用到STRRET結構,這要感謝StrretToString()函數提供的幫助,它當然也包含在Shell庫函數中。

讀取項目的圖示

    已開始接觸Shell程式設計,以為非常艱巨,然而,當你從初始的頭三四個月中挺過來之後,你就開始有機會接觸進階項目問題的答案了。要說明這一點,你認為怎樣才能取得一個項的圖示?回答是,你必須詢問提供它的檔案夾。IShellFolder::GetUIObjectOf()方法傳回所有你可能需要處理使用者界面和檔案對象的接口。

HRESULT IShellFolder::GetUIObjectOf(

HWND hwndOwner, // 視窗Handle

UINT cidl, // 下一個參數中的元素數

LPCITEMIDLIST* apidl, // 指向一個PIDLs數組的指針

REFIID riid, // 要求的接口ID

UINT* prgfInOut, // 保留(必須為NULL)

LPVOID* ppvOut // 接收接口的指針

);

對這個聲明我們感興趣的是可以請求一定數量的不同接口指針,它們都能影響UI顯示。例如,請求IContextMenu,以獲得元素的關聯菜單HMENU的Handle。在我們的任務中,請求的是IExtractIcon接口,以便查找圖示(在第十六章中将看到更多關于GetUIObjectOf()的解釋)。

pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),

IID_IExtractIcon, NULL, reinterpret_cast<LPVOID*>(&pExtractIcon));

IExtractIcon接口有兩個新方法:GetIconLocation()和Extract()。頭一個使你能知道圖示的索引和位置,第二個則傳回一個HICON 型的Handle。在用戶端調用GetIconLocation()函數時,它傳回包含圖示的檔案名,以及在這個檔案的資源中的一個從零開始的圖示索引。

HRESULT IExtractIcon::GetIconLocation(UINT uFlags,

LPSTR szIconFile,

INT cchMax,

LPINT piIndex,

UINT* pwFlags);

Extract()函數依次從指定檔案中抽取給定的圖示,和傳回它的HICON。這個方法幾乎與API函數ExtractIconEx()是一樣的。

HRESULT IExtractIcon::Extract(LPCSTR pszFile,

UINT nIconIndex,

HICON* phiconLarge,

HICON* phiconSmall,

UINT nIconSize);

對這些函數而言資料顯得有點冗長,例如,即使你不關心pwFlags的内容,你也需要知道它不能為NULL。類似地,即使你僅僅需要大圖示,你仍然必須為小圖示傳遞一個有效的非零HICON。下面測試一個例子,說明怎樣調用它:

pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH, &iIconIndex, &u);

pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon, &hIconSm, MAKELONG(32, 16));

pExtractIcon->Release();

在開發這個執行個體代碼時,我獲得了另一個有趣的結果,坦白地講,這超出了我的想象。在有些情況下Extract()函數傳回的Handle是NULL,即使在圖示的位置和索引都是正确的情況下,也是如此。奇怪,使用相同的參數調用ExtractIconEx()函數工作完好。當然,工作環境是直接的:

if(hIcon == NULL)

ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);

在這一點上,最後要做的工作就是需要建立一個新的Shell函數,它接收IShellFolder指針,在它的項上循環,為每個項喚醒回調函數。就像很多其他稱為‘枚舉’的函數一樣,我們的SHEnumFolderContent()函數将提供一個使用者定義的緩沖(dwData)發送程式級的變量到回調函數。進一步,如果回調函數傳回FALSE,則函數将停止工作。這裡是它的原型:

int SHEnumFolderContent(LPSHELLFOLDER pFolder,

FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl);

其中FOLDERCONTENTPROC是一個使用者定義的函數指針,其聲明是:

typedef BOOL (CALLBACK *FOLDERCONTENTPROC)(LPCSTR, HICON, DWORD);

第一個變量是元素顯示名,而後是一個圖示Handle。然後是使用者定義的緩沖。像已經提到的那樣,函數傳回FALSE來中斷枚舉,TRUE則繼續。

         SHEnumFolderContent()的最後一個參數是PIDL。這并不嚴格地必要,隻是有時需要(如在我們的例子中),了解這個最後傳遞的參數PIDL是有幫助的。如果這個變量是NULL,則它被忽略。下面是SHEnumFolderContent()函數的源代碼:

int SHEnumFolderContent(LPSHELLFOLDER pFolder,

FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl)

{

int iNumOfItems = 0;

// 枚舉内容

LPENUMIDLIST pEnumIDList = NULL;

pFolder->EnumObjects(

NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);

ULONG ulFetched = 0;

LPITEMIDLIST pItem = NULL;

while(NOERROR == pEnumIDList->Next(1, &pItem, &ulFetched))

{

STRRET sName;

TCHAR szBuf[MAX_PATH] = {0};

pFolder->GetDisplayNameOf(pItem, 0, &sName);

StrretToString(pItem, &sName, szBuf);

// 喚醒回調函數

if(pfn)

{

// 擷取圖示

UINT u = 0;

int iIconIndex = 0;

HICON hIcon = NULL;

HICON hIconSm = NULL;

TCHAR szIconFile[MAX_PATH] = {0};

LPEXTRACTICON pExtractIcon = NULL;

pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),

IID_IExtractIcon, NULL,

reinterpret_cast<LPVOID*>(&pExtractIcon));

pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH,

&iIconIndex, &u);

pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon,

&hIconSm, MAKELONG(32, 16));

pExtractIcon->Release();

if(hIcon == NULL)

ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);

if(!pfn(szBuf, hIcon, dwData))

{

// 傳回目前的PIDL

if(ppidl != NULL)

*ppidl = pItem;

break;

}

}

++iNumOfItems;

}

return iNumOfItems;

}

回調函數

    典型地,回調函數用于實作某些項的采集任務。如此,SHEnumFolderContent()調用這樣的函數采集各個檔案夾項。SearchText()函數僅僅報告是否傳遞過來的兩個串是相等的。

BOOL CALLBACK SearchText(LPCSTR pszItem, HICON hIcon, DWORD dwData)

{

return static_cast<BOOL>(lstrcmpi(pszItem, reinterpret_cast<LPCSTR>(dwData)));

}

ShowFolderContent()函數用于建構一個傳遞過來圖示的圖像清單,插入圖示到提供的清單觀察中:

BOOL CALLBACK ShowFolderContent(LPCSTR pszItem, HICON hIcon, DWORD dwData)

{

//建立圖像清單

int iIconWidth = GetSystemMetrics(SM_CXICON);

int iIconHeight = GetSystemMetrics(SM_CYICON);

if(g_himl == NULL)

g_himl = ImageList_Create(iIconWidth, iIconHeight, ILC_MASK, 1, 0);

int iIconPos = ImageList_AddIcon(g_himl, hIcon);

HWND hwndListView = reinterpret_cast<HWND>(dwData);

ListView_SetImageList(hwndListView, g_himl, LVSIL_NORMAL);

LV_ITEM lvi;

ZeroMemory(&lvi, sizeof(LV_ITEM));

lvi.mask = LVIF_TEXT | LVIF_IMAGE;

lvi.pszText = const_cast<LPSTR>(pszItem);

lvi.cchTextMax = lstrlen(pszItem);

lvi.iImage = iIconPos;

ListView_InsertItem(hwndListView, &lvi);

//更新計數

TCHAR s[MAX_PATH] = {0};

wsprintf(s, "%d item(s) found.", ListView_GetItemCount(hwndListView));

SetDlgItemText(GetParent(hwndListView), IDC_FOUND, s);

return TRUE;

}

示例程式

一定要保證,主程式代碼中包含#includes  shlobj.h 和resource.h,下面圖中顯示使用這一階段開發的執行個體程式可以做的操作。鍵入‘列印機’,你能充填這個清單觀察,就像标準的檔案夾視窗一樣:

Windows Shell程式設計-第五章.浏覽檔案夾

再有,指定一個路徑名,可以看到檔案和檔案夾,正象在探測器中所看到的那樣:

Windows Shell程式設計-第五章.浏覽檔案夾

         回想一下,如果想要得到任何驅動器根目錄的内容時,你必須包括一個最後的反斜杠‘/’,例如C:/工作正常,而C: 則産生這個結果:

Windows Shell程式設計-第五章.浏覽檔案夾

通過PIDL搜尋

         适當地使用我們所有的輔助函數,寫一個‘顯示PIDLs内容’按鈕的處理器不是太困難的事。與這個按鈕相關的combo框由名字和一些特殊檔案夾的ID初始化,這個過程執行與SHBrowse例子中相同的代碼。此處由函數在點選‘顯示PIDLs内容’按鈕時執行,并充填清單觀察:

void DoEnumeratePidl(HWND hDlg)

{

LPITEMIDLIST pidl = NULL;

// 取得特殊檔案夾和它的PIDL

HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);

int i = ComboBox_GetCurSel(hwndCbo);

int nFolder = ComboBox_GetItemData(hwndCbo, i);

SHGetSpecialFolderLocation(NULL, nFolder, &pidl);

//取得IShellFolder接口

LPSHELLFOLDER pFolder = NULL;

SHGetDesktopFolder(&pFolder);

// 綁定到子檔案夾

LPSHELLFOLDER pSubFolder = NULL;

pFolder->BindToObject(pidl, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

pFolder->Release();

pFolder = pSubFolder;

//清除程式的UI

ClearUI(hDlg);

//枚舉内容

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

SHEnumFolderContent(pFolder, ShowFolderContent,

reinterpret_cast<DWORD>(hwndListView), NULL);

// 清理

LPMALLOC pMalloc = NULL;

SHGetMalloc(&pMalloc);

pMalloc->Free(pidl);

pMalloc->Release();

pFolder->Release();

}

這個函數從獲得特殊檔案夾的ID開始,這個ID來自combo框的選擇,而後調用SHGetSpecialFolderLocation()函數獲得檔案夾的PIDL,再從這個PIDL獲得IShellFolder接口,然後傳遞給SHEnumFolderContent()函數。圖中顯示了這個應用怎樣枚舉‘控制台’的小程式:

Windows Shell程式設計-第五章.浏覽檔案夾

特殊檔案夾

         我們首先在第二章中看到了特殊檔案夾和它們的基本概念,其中有三種基本類型。幾乎所有的特殊檔案夾都有對應的目錄,但是這些與普通的檔案型檔案夾和客戶檔案夾是完全不同的。第三種類型是由沒有目錄的檔案夾(虛拟檔案夾)組成。

虛拟檔案夾感覺上象一個檔案夾,但是它們的位置和内容沒有檔案和目錄概念的映射。‘控制台’,‘列印機’,‘網路上的芳鄰’,‘我的計算機’都是虛拟檔案夾的例子。例如‘控制台’可以包含所有安裝了的小程式。

         除了外觀,沒有稱為‘控制台’的實體目錄可以包含與之相關的任何東西,比如‘添加新硬體’或‘Modems’。這個檔案夾所列出的所有圖示都來自系統目錄下的.cpl檔案。他們由命名空間擴充聚集和表示為虛拟檔案夾。

系統對特殊檔案夾的支援

         Windows API定義了一定數量的特殊檔案夾,并且有一堆函數作用其上。這些例程通過如ID一樣的數字辨別每一個特殊檔案夾,但是對PIDLs或CLSIDs則沒有什麼可做的。

         ID定義在shlobj.h中,并且都有相當奇怪的符号名:都以CSIDL_開始。下表列出了一些可用的特殊檔案夾:

檔案夾ID 虛拟的 描述
CSIDL_DESKTOP Yes 桌面
CSIDL_DRIVES Yes 我的計算機
CSIDL_BITBUCKET Yes 資源回收筒
CSIDL_CONTROLS Yes 控制台
CSIDL_NETWORK Yes 網路上的芳鄰
CSIDL_INTERNET Yes Shell上的IE探測器節點(版本4.71以上)
CSIDL_PRINTERS Yes 列印機
CSIDL_DESKTOPDIRECTORY 含有全部桌面快捷方式的目錄
CSIDL_FAVORITES Favorite檔案夾的快捷方式
CSIDL_FONTS 安裝的字型
CSIDL_NETHOOD 網絡域的引用
CSIDL_PRINTHOOD 列印機的引用
CSIDL_PERSONAL 私有檔案的快捷方式
CSIDL_PROGRAMS ‘程式’菜單的快捷方式
CSIDL_RECENT 最近使用文檔的快捷方式
CSIDL_SENDTO ‘發送到’菜單項的快捷方式
CSIDL_STARTMENU ‘開始’菜單中使用者定義的項
CSIDL_STARTUP 啟動時運作的程式的快捷方式
CSIDL_COOKIES Cookies
CSIDL_TEMPLATES 文檔模版的快捷方式
CSIDL_HISTORY 通路過的Web頁面的快捷方式
CSIDL_INTERNET_CACHE IE的臨時Internet檔案
CSIDL_APPDATA 一個應用專有資料的檔案夾
CSIDL_ALTSTARTUP 非本地‘啟動’組

資料中還提到了另外一些标号為CSIDL_COMMON_XXX的檔案夾,它們是:

CSIDL_COMMON_STARTUP            CSIDL_COMMON_STARTMENU

CSIDL_COMMON_PROGRAMS           CSIDL_COMMON_FAVORITES

CSIDL_COMMON_DESKTOPDIRECTORY   CSIDL_COMMON_ALTSTARTUP

除了它們指向一個任何使用者都可看到的實體檔案夾以外,這些檔案夾與不包含COMMON的檔案夾相同。雖然這一點在資料中沒有顯式提到,這些檔案夾似乎感覺僅在WindowsNT下出現。

擷取檔案夾路徑

         非虛拟檔案夾在機器的某個地方有一個路徑,你可以通過調用SHGetSpecialFolderPath() API函數獲得一個特殊檔案夾的路徑。特殊檔案夾與它的路徑之間的連接配接存儲在系統資料庫中,注冊鍵入下:

HKEY_CURRENT_USER

/Software

/Microsoft

/Windows

/CurrentVersion

/Explorer

/Shell Folders

在HKEY_LOCAL_MACHINE下相同的鍵存儲了所有可用的COMMON檔案夾,但是,在Windows95和Windoes98下并不是所有COMMON檔案夾都有路徑。事實上,僅僅CSIDL_COMMON_DESKTOPDIRECTORY和CSIDL_COMMON_STARTUP有路徑存儲。假設C:/Windows是系統目錄,所列出的路徑在C:/Windows/All Users folder下。然而,在Windows95/Windows98下SHGetSpecialFolderPath()函數不能為其傳回任何值。反之,在WindowsNT下使用同樣的函數,則傳回正确的路徑。

函數

         我們可以通過觀察曾經使用過的函數SHGetSpecialFolderLocation()來走進函數SHGetSpecialFolderPath()函數。這個函數恢複指定的特殊檔案夾的PIDL,它有下面的原型:

HRESULT SHGetSpecialFolderLocation(HWND hwndOwner,

int nFolder,

LPITEMIDLIST* ppidl);

hwndOwner是任何彈出顯示視窗的父視窗,nFolder是特殊檔案夾的辨別符,可以是上表列出的常量之一,而ppidl則是指向包含這個檔案夾的PIDL緩沖的指針。SHGetSpecialFolderPath()函數,試圖恢複給定檔案夾的路徑,與上函數非常相似:

HRESULT SHGetSpecialFolderPath(HWND hwndOwner,

LPTSTR lpszPath,

int nFolder,

BOOL fCreate);

lpszPath将包含路徑名,而fCreate是一個邏輯值,表示如果檔案夾不存在,是否需要建立。當然,在這種情況下,你不能指定一個虛拟檔案夾的ID。注意,與SHGetSpecialFolderLocation()函數不同,SHGetSpecialFolderPath()僅僅支援Shell 4.71以上版本。

檔案夾設定

         IE4.0和活動桌面極大地增加了系統檔案夾的設定量。‘檔案夾選項’對話框有完善的複選框來确定檔案夾的外觀,使其具有我們希望的形式:

Windows Shell程式設計-第五章.浏覽檔案夾

這個對話框是Windows系統上人人都使用的對話框,它使你能設定檢視系統檔案和隐藏檔案。有些設定(不是全部)可以通過程式設計讀出,自然這些也僅能在Shell 4.71以上版本上有效。

         在VC++的資料中可以找到每一個設定的詳細說明,在這裡所能找到的僅僅是一個例子程式。

SHGetSettings()函數

         實際上,使用SHGetSettings()函數是相當簡單的,它僅需要兩個變量:

void SHGetSettings(LPSHELLFLAGSTATE lpsfs, DWORD dwMask);

SHELLFLAGSTATE是一個非常簡潔的結構:

typedef struct

{

BOOL fShowAllObjects : 1;

BOOL fShowExtensions : 1;

BOOL fNoConfirmRecycle : 1;

BOOL fShowSysFiles : 1;

BOOL fShowCompColor : 1;

BOOL fDoubleClickInWebView : 1;

BOOL fDesktopHTML : 1;

BOOL fWin95Classic : 1;

BOOL fDontPrettyPath : 1;

BOOL fShowAttribCol : 1;

BOOL fMapNetDrvBtn : 1;

BOOL fShowInfoTip : 1;

BOOL fHideIcons : 1;

UINT fRestFlags : 3;

} SHELLFLAGSTATE, *LPSHELLFLAGSTATE;

dwMask參數是二進制屏蔽位—對于結構中每一個感興趣的字段和需要函數恢複的字段,都必須設定适當的屏蔽位。可能的值是:

字段 屏蔽位 在檔案夾選擇對話框中的設定
fShowAllObjects SSF_SHOWALLOBJECTS 顯示所有檔案
fShowExtensions SSF_SHOWEXTENSIONS 隐藏已知檔案類型的檔案擴充
fNoConfirmRecycle SSF_NOCONFIRMRECYCLE None
fShowSysFiles SSF_SHOWSYSFILES 不顯示隐藏檔案
fShowCompColor SSF_SHOWCOMPCOLOR None
fDoubleClickInWebView SSF_DOUBLECLICKINWEBVIEW 在‘一般|客戶設定’對話框上‘輕按兩下打開項’選擇
fWin95Classic SSF_WIN95CLASSIC 在‘一般’頁上‘典型風格’選擇
fDontPrettyPath SSF_DONTPRETTYPATH 全部允許大寫名字
fMapNetDrvBtn SSF_MAPNETDRVBUTTON 在工具條中顯示‘網絡驅動器映射’按鈕
fShowAttribCol SSF_SHOWATTRIBCOL 在‘細節觀察’中顯示檔案屬性
fShowInfoTip SSF_SHOWINFOTIP 對檔案夾和桌面項顯示彈出的描述
fDesktopHTML SSF_DESKTOPHTML 在活動桌面關聯菜單上設定‘Web頁面觀察’
fHideIcons SSF_HIDEICONS 當桌面作為Web頁面時,隐藏圖示

很多資料都說明fHideIcons是沒用的,事實上它僅僅說明當桌面設定成Web模式時,是否桌面上的圖示應該被顯示。現在讓我們檢視一些應用,以便探讨可以從這些标志中獲得的資訊。

觀察檔案擴充

         頭一個用途是程式員是否想要在使用者的應用界面中顯示檔案擴充。如果應用程式需要在任何情況下顯示檔案名,你就應該依據這個标志的狀态使使用者能夠選擇确定是否顯示擴充名。

使桌面更具活力

         fHideIcons标志使你在觀察模式設定為‘Web頁面’時知道桌面上的圖示是否可以看到。而fDesktopHTML标志從另一方面告訴你是否桌面使用了一個HTML頁作為它的背景。如果桌面是在Web模式中,并且圖示不可見,則你就不能在桌面上建立新的快捷方式。

         如果我們僅僅能設定這些位而不能獲得它們的狀态的話,組合使用fDesktopHTML和fHideIcons是非常有用的。考慮下面的情況:有很多方法清除桌面以禁止公共計算機使用者而不是你浏覽和運作應用程式。而使用fDesktopHTML和fHideIcons的組合給出了一種新方法。首先,它允許你設定标志顯示HTML頁面作為桌面的背景,其次隐藏所有桌面上的圖示。使用這種方法,你可以把Windows桌面(和機器)變成一個專門運作單一HTML應用的伺服器。誠然,任務條還在,但是,你可以通過取得其HWND,然後使用SW_HIDE标志調用ShowWindow()函數來隐藏它:

// 任務條是一個視窗類'Shell_TrayWnd'

HWND hwnd = FindWindow("Shell_TrayWnd", NULL);

if(IsWindow(hwnd))

ShowWindow(hwnd, SW_HIDE);

點選清單觀察

         在Shell 4.71以上版的衆多檔案夾的設定中,有可能設定檔案夾的行為使其在被選中時變成下劃線形式,和隻要一次點選便可打開它們。你可以通過‘檔案夾選項’對話框的‘一般’頁設定這些選擇。有趣的是,這些風格對于通用控件庫版本4.70的清單觀察也有效。是以你可以根據fDoubleClickInWebView标志來修改清單觀察的活動形式和滑鼠跟蹤能力。如此,需要考慮下列關系:

LVS_EX_ONECLICKACTIVATE (4.70)

LVS_EX_TWOCLICKACTIVATE (4.70)

LVS_EX_UNDERLINECOLD (4.71)

LVS_EX_UNDERLINEHOT (4.71)

這個清單中的版本号指的是通用控件庫的版本号,不是Shell的版本号。版本4.70的comctl32.dll與IE4.0一同釋出(無論活動桌面是否安裝)。4.71與IE4.01。

         設定擴充風格的清單觀察,需要使用ListView_SetExtendedListViewStyle()函數,它是一個圍繞LVM_SETEXTENDEDLISTVIEWSTYLE消息建立的宏。上面清單中的頭兩種風格的意義是直接的,其它的涉及到熱點項(一個術語,用于描述滑鼠正在經過的項)。LVS_EX_UNDERLINECOLD引起非熱點項下劃線,而LVS_EX_UNDERLINEHOT則僅僅熱點項下劃線。

删除操作的确認

         fNoConfirmRecycle标志訓示在删除檔案之前是否顯示确認對話框。可以想象,這僅适用于删除到‘資源回收筒’和Shell操作的場合。然而,即使你沒有使用Shell函數,如SHFileOperation(),來删除檔案,如果使用者希望有這樣的詢問時,是否不能很好地給出确認提示呢?閱讀一下fNoConfirmRecycle可以向這個方向前進一大步,進而使之成為可能。

示例程式

    程式界面,作為這一章最後一個例子,如下圖所示,可能你已經猜到了什麼是我們要用于建立的程式架構。

Windows Shell程式設計-第五章.浏覽檔案夾

這個例子的代碼是十分容易的:隻需要為‘取得設定’按鈕加一個處理器即可,它将引起目前Shell的選項設定被讀出。下面的源碼産生了在圖中所看到的結果。總之,一定要記住在源檔案頂部包含#include shlobj.h 和 resource.h。

void OnSettings(HWND hDlg)

{

SetDlgItemText(hDlg, IDC_SETTINGS, "");

SHELLFLAGSTATE sfs;

SHGetSettings(&sfs, SSF_DESKTOPHTML | SSF_SHOWALLOBJECTS |

SSF_MAPNETDRVBUTTON | SSF_SHOWATTRIBCOL | SSF_SHOWEXTENSIONS);

TCHAR szBuf[MAX_PATH] = {0};

if(sfs.fDesktopHTML)

lstrcat(szBuf, __TEXT("Active Desktop - View as Web page is active/r/n"));

if(sfs.fMapNetDrvBtn)

lstrcat(szBuf, __TEXT("Network buttons on the toolbar/r/n"));

if(sfs.fShowAllObjects)

lstrcat(szBuf, __TEXT("Shows all files/r/n"));

if(sfs.fShowAttribCol)

lstrcat(szBuf, __TEXT("Shows attributes in Detail view/r/n"));

if(sfs.fShowExtensions)

lstrcat(szBuf, __TEXT("Shows extensions for known file types/r/n"));   

SetDlgItemText(hDlg, IDC_SETTINGS, szBuf);

}

參數設定

         閱讀了這些參數的設定,對處理各種情況确實有不少幫助,但是更有興趣的應該是程式設計地設定這些特征。不幸地是,到目前為止隻有SHSetSettings()例程出現,現在,我們要說明的是有大量的可以達到這個目标的方法可用而不需要微軟的幫助。

參數存儲在哪裡

         正如你已經大緻猜到的那樣,所有可以使用SHGetSettings()讀到的參數都存儲在系統資料庫的某個地方。也即,可以由一個相對安全的方法來程式設計地設定這些參數。

    在向下進行之前,我們要強調一個重點。在官方資料缺乏的情況下,微軟在未來的作業系統版本中是可以自由改變系統資料庫鍵的用途的,這就間接地影響到你的代碼。在編寫代碼時,我們使用的技術在版本4.71下是好用的。

         打開系統資料庫,檢視下面的鍵:

HKEY_CURRENT_USER

/Software

/Microsoft

/Windows

/CurrentVersion

/Explorer

/Advanced

Windows Shell程式設計-第五章.浏覽檔案夾

似乎我們已經找到了要找的東西。修改這些系統資料庫項是足夠簡單的,對嗎?很不幸,不是這樣的—你很快就能注意到,這個值清單缺少了條目數。尤其是‘Web觀察’設定。

         回想一下測試反向工程系統資料庫設定的黃金定律:總是比較HKEY_CURRENT_USER和 HKEY_LOCAL_MACHINE下相同鍵的内容。這裡就是我們要找的:

Windows Shell程式設計-第五章.浏覽檔案夾

正如所見,有一個完整的層次結構複制了與‘檔案夾選項’對話框相同的樹結構。主節點是一個“group”類型的節點,有自己的Bitmap,和顯示名。結構的葉含有一個屬性集,其中突出的有兩個值:RegPath和 HKeyRoot:

Windows Shell程式設計-第五章.浏覽檔案夾

這就是說,在子樹中每一個條目都指向系統資料庫中的另一個鍵,實際值就存儲在那兒,其路徑是HKeyRoot/RegPath/ValueName。葉特征确定了顯示文字,選項類型(複選框或收音按鈕),選擇值(選中或沒選中),預設值,甚至檔案名和有幫助的主題ID等。

         給出了這些之後,處理客戶的SHSetSettings()函數就隻是簡單地讀寫系統資料庫資料操作了。

附加客戶選項到标準對話框

         因為在‘檔案夾選項’對話框的層次結構和系統資料庫子樹布局之間有良好的對應,我們能立即猜測到添加一個新鍵到系統資料庫應該在标準對話框中産生一個新的客戶選項。為了證明這一點,僅需要做一件事情:加一個新鍵到系統資料庫子樹。

         我在Folder下定義了一個新鍵,稱為MySetting。然後定義所有在其它葉上看到的值:

Windows Shell程式設計-第五章.浏覽檔案夾

儲存這個改變到系統資料庫之後,期待地打開‘檔案夾選項’對話框,沒有任何新東西出現。事實上,對此有一個顯而易見的原因:對話框僅在能夠讀出所存儲的值時才添加新項。正如早先提到過的那樣,這個值存儲在系統資料庫的另一個位置—是由HKeyRoot,RegPath 和ValueName指向的。剩下的就是在下面的鍵上建立一個稱為MySetting的新值:

HKEY_CURRENT_USER

/Software

/Microsoft

/Windows

/CurrentVersion

/Explorer

/Advanced

還應該設定所期望選項的預設值。在儲存改變和響應‘檔案夾選項’對話框後,新的設定就出現了,如下圖所示:

Windows Shell程式設計-第五章.浏覽檔案夾

什麼時候客戶選項是有用的

         在‘檔案夾選項’對話框中添加新的客戶選項不是為了向人炫耀—也不是一項允許使用者客戶化你的程式的便利方法。我們并不建議你使用這個對話框來設定一個應用的所有設定。但是,考慮使用選項來解決使用者界面和檔案夾的問題是有價值的。能更好地探索這些特征的子產品應該在命名空間擴充應用中。

使用系統資料庫路徑的選擇完全在于你自己,但是,應該認識到這一點,把你自己的設定存儲到遠離标準設定的地方,好的選擇是使用應用特殊的注冊鍵。

小結

         檔案夾是一個廣泛的題目,這一章一直在努力給出詳細解釋。你已經看到怎樣浏覽特殊檔案安夾和怎樣與它們一道工作。枚舉它們的内容和設定它們的參數。特别,在這一章中還揭示了:

         怎樣更好地使用SHBrowseForFolder()

    怎樣枚舉任何檔案夾的内容

    處理特殊系統檔案夾的函數

    那些檔案夾設定是可讀的,怎樣程式設計設定它們

為此,我們建構了潛在有用的函數來擴充API所提供的工具。例如SHEnumFolderContent()和 SHPathToPidlEx()輔助例程。此外,我們還揭示了Shell怎樣存儲檔案夾的設定,和給出了添加新選項到标準的‘檔案夾選項’對話框的方法。

引用:http://blog.csdn.net/chchzh/article/details/2276849