第十六章 命名空間擴充
探測器使用層次結構表述形成系統的許多對象——檔案,檔案夾,列印機,網絡對象等等。這些對象組合定義了一個命名空間,這是一個封閉的符号或名字集合,其中任何給定的符号或名字都能成功地被喚醒。在命名空間中解析一個名字就是成功地連接配接給定的名字到某個它所表述的實際資訊。
探測器仔細地把所有這些對象收集到一起,與它們通訊,把它們的内容顯示在典型的兩窗框視窗中,樹狀觀察在左側,清單觀察在右側。
我們實際所關注的是探測器是否支援插入代碼到它的結構中并增加全新定制對象的接口。事實上,Windows本身就伴随一定數量的命名空間擴充,例子包括‘撥号網絡’,‘我的公文包’,以及‘我的計算機’檔案夾。在這一章中,我們打算解釋整個命名空間是怎樣工作的,帶你一起輕松遊曆其中的代碼。
命名空間擴充實際是一個巨大的課題,然而卻不難找到關于這個課題的文章,許多文章都隻就上述兩個方面之一進行讨論,要麼解釋基礎,提供大量的自由代碼來觀察整個機理,要麼集中于編碼,讨論技巧和方法,而不提供命名空間擴充工作的全面解釋,這裡,我們依序解釋下面各項内容:
命名空間擴充概覽
安裝命名空間擴充
編寫完整的命名空間擴充來浏覽所有目前打開視窗的層次結構
定義客戶PIDLs的規則
開發命名空間擴充使客戶應用駐留在探測器中
命名空間擴充是建立在本身并不特别複雜的概念之上的,然而,其極其豐富的資料,程式設計方法,實作特征,以及要求的技巧使編寫這些任務遠不是一般程式任務所能勝任的。即使已經有一個擴充安裝并運作了,要想最終完成它也仍然有許多要做的工作。有非常多的附加特征可能需要你付出雙倍的的時間和精力來完成。
公平地講,空的命名空間擴充并不比我們前一章講的Shell擴充複雜。麻煩是,在絕大多數情況下,空命名空間擴充是相當沒用的。
命名空間擴充概覽
命名空間擴充最容易的定義是:
命名空間擴充是允許擴充和定制被內建到Windows探測器中資訊的一個方法。
‘內建’的基本意義是資訊被顯示和處理的方法與其它标準資訊被顯示和處理的方法一樣。同義于‘命名空間擴充’的應該是‘客戶繪制’檔案夾——命名空間擴充包含通路和展示資料的代碼,并試探與探測器進行內建。後面一步是相當标準的代碼段,盡管被封裝在一個類的集合中,它仍然是具有挑戰意義的。
探測器顯示的資訊可能與實體目錄相關,也可能不相關——例如,考慮‘撥号網絡’檔案夾,其中有關于Internet連接配接資訊,或‘列印機’檔案夾,其中包含了安裝的列印機詳細資訊。其它命名空間擴充的例子是以非标準的方式顯示檔案資料,如‘資源回收筒’或‘臨時Internet檔案’檔案夾。
我們可以在命名空間擴充和通常意義檔案夾之間确定三個層面上的差異:
觀察,即,探測器右側視窗的内容
菜單(和可能的工具條)
其它次要的圖像變換,如樹觀察的圖示和狀态條上的客戶文字
其中最重要的是顯示在觀察中的定制内容。盡管‘資源回收筒’使用清單觀察來顯示其内容,但這僅是一個選擇——在你的擴充中,可以使用任何種類的視窗,隻要你喜歡(或許清單觀察可能是你可以使用的最靈活的視窗)。
編寫寫命名空間擴充意味着什麼
命名空間擴充在探測器中呈現一個定制的檔案夾。它是一個Shell感覺的程序内COM對象,倘若你正确地注冊了它。命名空間擴充實作了一捆接口,探測器回調這些接口來獲得它需要适當設定這個檔案夾觀察的所有資訊。典型地,探測器要求:
檔案夾管理對象,通過這個對象,回答探測器的請求
顯示檔案夾内容的視窗
枚舉檔案夾所包含各種項的對象
唯一地辨別檔案夾各個項的方法
客戶化使用者界面的附加功能集
下圖說明了探測器的體系結構:
下圖說明了與命名空間擴充的關系:
在探測器感覺到一個命名空間擴充存在時(後面我們将進一步精确解釋它是怎樣做到的),它就裝入這個COM伺服器,并查詢IShellFolder接口。這是一個作為檔案夾管理器工作的接口,并且向探測器提供它所要求的所有東西。換句話說,它是在探測器與其它擴充之間的一個代理。
當探測器需要顯示觀察内容時,它請求IShellFolder給出觀察對象。類似的,在顯示樹觀察的節點時,它請求枚舉内容和查找檔案夾和子檔案夾屬性,所有這些都是通過IShellFolder接口來做的。
命名空間擴充被裝入後,探測器也給它機會來更新使用者界面。所有可能感興趣的事件都通過調用指定接口的适當函數通知到擴充。
反過來,寫一個命名空間擴充實際就是準備回答來自探測器的所有輸入呼叫,而回答呼叫就是在特定的COM接口上實作一定的功能。正如我們想象的那樣,有一個擴充必須支援的最小接口和函數集以使其能夠很好地被內建。
探測器内部結構
探測器僅僅是一個由空架構,如樹觀察,清單觀察和幾個其它控件組成的普通程式,它完全依賴于Shell和命名空間擴充來為其基本骨骼添加實在的血肉。實際顯示的每一件東西都來自于explorer.exe檔案之外。标準的擴充是在shell32.dll中實作的,這使其成為一類系統代碼,然而,它們确切的是命名空間擴充。探測器掃描系統資料庫來安裝部件和打開與它們的通訊,不管它們是你自己寫的還是作業系統提供的。
命名空間擴充與Shell擴充
在原理上,Shell擴充和命名空間擴充确實是相當類似的。二者都需要注冊以便被感覺和被喚醒。二者都是程序内COM伺服器,實作固定數量的接口,而且二者都允許Shell客戶化。最大的差别是它們最終産生的效果:命名空間擴充把新檔案夾加入到探測器,而Shell擴充被限制在檔案類型上工作。
主接口
現在我們已經有點了解了在命名空間擴充運作時會發生什麼情況,下面我們來看一下實際所發生的。這給我們一個機會來檢視接口和一些函數原型。後面我們将使用這些資訊來構造我們的例子。命名空間擴充絕對需要實作的接口是:
! IShellFolder
! IPersistFolder
! IEnumIDList
! IShellView
頭兩個就是我們前面說過的‘檔案夾管理器’。IEnumIDList是我們稱之為‘枚舉器’的東西,而IShellView主要是提供觀察視窗,這是将替代标準的清單觀察的視窗。
這四個主要的接口(以及某些次要接口)都包含有PIDL的概念,我們在第2章就已經精确地解釋了PIDL,以及它的實作,我們在這裡重新概括一下:PIDL是在整個Shell命名空間中明确表示一個檔案夾項的辨別符,PIDL是對一類檔案夾特定的,并且在寫一個‘客戶檔案夾’時,你也應該提供一個‘客戶’PIDL。雖然在做這個時有幾個基本規則要遵循,但還是沒有設計PIDL的一般方法,它十分依賴于它所要輔助呈現的内容。我們還要進一步解釋這個概念。
還有一些其它可選的COM接口是命名空間擴充可以實作的,實際上,它們就是IContextMenu 和IExtractIcon,處理定制的關聯菜單和單個項的圖示。
下一節我們給出清單,說明各個接口定義的函數。如果有理由避免實作它們的話,其名字是由斜體字給出的。為了避免實作,和為了使探測器知道繼續操作,需要傳回E_NOTIMPL錯誤碼。
活動時序
在我們結束關于接口的叙述之前,有一些觀念應該記住。在使探測器顯示命名空間擴充期間給出它們之間通訊的描述。必須繪制事件的時序關系:
探測器通過連接配接點感覺命名空間擴充,并且取得它的CLSID。
探測器建立它的執行個體,并且查詢IShellFolder接口。
探測器請求實作IShellFolder的對象傳回指向IShellView接口的指針于觀察對象上。
指向IShellBrowser的指針被傳遞到觀察對象,允許它處理探測器的菜單和工具條。觀察對象也接收指向IShellFolder的指針。
探測器請求IShellFolder對象傳回枚舉檔案夾内容的對象,這個對象實作IEnumIDList接口。
探測器周遊包含在檔案夾中的元素,為每一個項取得PIDL,并根據其角色和特征繪制圖示。
這是在探測器樹觀察中選擇了命名空間擴充節點後所發生的操作。當你點選展開時,探測器作下面的操作:
請求IShellFolder傳回枚舉檔案夾内容的對象。
顯示那些有‘檔案夾’屬性的元素。如果它含有子檔案夾特征,則繪制一個‘+’節點。
請求IShellFolder提供樹觀察中所屬每一個節點顯示的圖示(事實上,它接收一個指向IExtractIcon接口的指針)。
請求IShellFolder提供每一項的顯示文字
請求IShellFolder提供每一項的關聯菜單。
檔案夾管理器
IShellFolder是從IPersistFolder導出的,而IPersistFolder依次從IPersist導出。IPersistFolder的功能是允許探測器初始化新檔案夾,告訴它在命名空間中的位置。IShellFolder特有的函數以探測器可以請求觀察對象、枚舉對象,或子檔案夾的方式組成程式設計接口,進一步,IShellFolder對象必須能夠提供它包含的每一個單項的屬性,兩個項的比較,以及傳回它們的顯示名。項通過PIDLs辨別。
IpersistFolder接口
下表給出了IPersistFolder接口的函數:
函數 | 描述 |
GetClassID() | 傳回對象的 CLSID。這個方法來自于IPersist。 |
Initialize() | 允許檔案夾初始化它自己。這個方法接收PIDL來辨別檔案夾在命名空間中的位置。這個方法不一定相關于檔案夾,如果相關,它應該緩存這個PIDL,以備進一步的使用,否則隻需簡單地傳回S_OK即可。 |
一般應該實作這兩個方法,它們的原型和典型的(最小)代碼如下:
STDMETHODIMP CShellFolder::GetClassID(LPCLSID lpClassID)
{
*lpClassID = CLSID_WinView;
return S_OK;
}
STDMETHODIMP CShellFolder::Initialize(LPCITEMIDLIST pidl)
{
return S_OK;
}
這段代碼取自本章中一個例子,現在,我們必須清楚CLSID_WinView是擴充本身的CLSID辨別,而CShellFolder則是由IShellFolder和IPersistFolder導出的C++的類名。
你絕不應該直接調用IPersistFolder的方法,相反,系統在綁定它到你的檔案夾過程中調用它們。
IShellFolder接口
IShellFolder接口輸出的十個函數清單說明如下:
函數 | 描述 |
BindToObject() | 這是Shell請求子產品打開子檔案夾的方式。這個方法接收一個PIDL,應該簡單地建立一個基于所接收PIDL的新檔案夾對象。 |
BindToStorage() | 目前。Shell從不喚醒這個方法,是以隻需傳回E_NOTIMPL即可。 |
CompareIDs() | 攜兩個PIDLs,并決定它們的順序——一個大于另一個,或它們是相等的。 |
CreateViewObject() | 建立和傳回IShellView對象,這個對象将提供右窗框内的顯示内容。 |
EnumObjects() | 建立和傳回IEnumIDList對象,這個對象提供項的枚舉操作。 |
GetAttributesOf() | 傳回指定項的屬性族——它是否可以重命名或拷貝,它是否有一個精靈圖示,它是否是一個檔案夾或有子檔案夾,這些有效的常量都有以SFGAO_開始的助記符。資料中有完整的清單。 |
GetDisplayNameOf() | 傳回顯示項的名字,用于檔案夾,位址欄或解析的目的。需要項名的應用把項名作為一個變量進行傳遞。值是SHGNO枚舉類型。 |
GetUIObjectOf() | 探測器使用這個方法請求特定的接口,這些接口必須與UI一同操作。它是一種更特殊的QueryInterface()。 |
ParseDisplayName() | 傳回一個PIDL給定的顯示名。然而,這個顯示名并不是必須出現在Shell觀察中或位址欄中的。它是在SHGDN_FORPARSING标志設定後由GetDisplayNameOf()傳回的。 |
SetNameOf() | 指派一個新的顯示名到給定的對象。這是要使用在位址欄,檔案夾或解析目的中的名字。 |
我們在第4章中解釋過顯示名,但是主要是在Shell内用于顯示項的名字,在大多數場合,這個顯示名與實際的檔案名一緻,然而對于不包含檔案的檔案夾,它可能就不一緻了。有三種類型的顯示名用于三種不同的關聯。它們都來自下面的枚舉類型:
typedef enum tagSHGDN
{
SHGDN_NORMAL = 0, // 相關于桌面的名字
SHGDN_INFOLDER = 1, // 相關于檔案夾的名字
SHGDN_INCLUDE_NONFILESYS = 0x2000, // 非檔案系統對象
SHGDN_FORADDRESSBAR = 0x4000, // 用于位址欄
SHGDN_FORPARSING = 0x8000, // 用于解析
} SHGNO;
在PIDL唯一的辨別每一個項期間,它可以在不同的場合用不同的名字顯示。為了在任何場合傳回顯示名,你應該實作GetDisplayNameOf()。這個函數接收值為SHGNO值組合的參數,特别是這個函數可以被要求傳回絕對(SHGDN_NORMAL)名或相對名(SHGDN_INFOLDER)。前者,你确切地傳回相對于桌面的顯示名,而後者則要求相對于父檔案夾的名字。
除此之外,可能還有關于Shell使用的名字的更多标志,這給出适當調整名字的機會。當名字被顯示在位址欄的時候,SHGDN_FORADDRESSBAR标志設定,當你感覺到SHGDN_FORPARSING的時候,說明名字要被傳遞到ParseDisplayName(),來轉換成一個PIDL。你可能需要包含特殊資訊來輔助這個任務。
SHGDN_INCLUDE_NONFILESYS位簡單地讓這個方法知道調用者想要非檔案系統對象。如果傳遞的PIDL不是檔案系統對象,并且這個位沒有設定,這個方法失敗。
通過IShellFolder方法,探測器能夠獲得它所需要的關于擴充的任何資訊。任何要求的接口都通過這個接口的方法獲得:Shell觀察,關聯菜單,圖示處理器,以及項枚舉器等。
接口 | 通過函數獲得 |
IShellView | CreateViewObject() |
IContextMenu | GetUIObjectOf() |
IExtractIcon | GetUIObjectOf() |
IEnumIDList | EnumObjects() |
項的枚舉
寫一個命名空間擴充來嵌入客戶檔案夾到Shell中,這個虛拟的(不是實體的)檔案夾可能包含一些需要用非标準方式顯示的内容。或者說,它包含了一些非标準的内容需要象檔案對象清單那樣顯示。一個假想檔案夾如‘我的硬體’可以包含對各種依附于系統的裝置的引用。這個資訊可以表示為一個清單觀察,其中的裝置作為顯示項。
無論内容多麼特殊,它都能由一個元素集組成,盡管命名空間擴充之外沒有必要知道這個事實,不過,探測器需要枚舉這些對象以便繪制樹觀察。為了允許外部子產品周遊客戶檔案夾的内容,命名空間擴充應該實作IEnumIDList接口。這是一個函數集,它對外部子產品提供枚舉檔案夾各個項的能力。這個接口及其普通,子產品可以與它通訊,不需要知道檔案夾本身内容群組織形式。
IEnumIDList接口
IEnumIDList接口輸出四個函數,以便在給定集合中前後移動。
函數 | 描述 |
Next() | 傳回集合中指定數量的項,每一個被找到的項都由PIDL辨別。 |
Skip() | 跳過指定數目的項。 |
Reset() | 移動目前指針到清單頂部。 |
Clone() | 複制一個對象。 |
關鍵函數是Next(),它的原型如下:
HRESULT IEnumIDList::Next(ULONG celt,
LPITEMIDLIST* rgelt,
ULONG* pceltFetched);
函數的第一個變量指定要恢複的項數,而這些項的PIDLs将存儲在rgelt數組中。實際拷貝的元素總數則存儲在第三個變量pceltFetched中。枚舉器對象處理所有項的一個連結清單。因而,完整實作應該儲存一個指向目前項的指針,并且由Next()恢複的項數來移動它。
Skip()方法也由傳遞給它的變量向前移動項指針。但是它并不實際恢複和讀出這些項的内容:
HRESULT IEnumIDList::Skip(ULONG celt);
Clone() 和 Reset()是這個對象的輔助方法,下面是這兩個函數的典型實作:
STDMETHODIMP CEnumIDList::Reset()
{
m_pCurrent = m_pFirst;
return S_OK;
}
STDMETHODIMP CEnumIDList::Clone(IEnumIDList** ppEnum)
{
return E_NOTIMPL;
}
通常,這個接口的方法建築在連結清單操作之上,連結清單則在類實作的初始化期間形成。連結清單中的每一個項指向一個PIDL。
PIDL的重要性
使用IEnumIDList的函數任何人都可以操作所有檔案夾的内容。在命名空間擴充中,一個實作了IEnumIDList接口的對象作為調用IShellFolder::EnumObjects()的結果被傳回。然而,一般枚舉項的接口并不足以正确辨別每一個檔案夾的項,而且是以PIDLs适合的形式。
正向我們在第2章中所解釋的,PIDL是一個指向SHITEMID結構集的指針。它使你能在一個檔案夾中容易且清晰地辨別任何給定對象的相對或絕對路由。說一個路由是相對的,如果它從包含項的檔案夾開始,而絕對路由則是從桌面開始的一系列引用,并且直到這個對象。在整個Shell中PIDL總是唯一地辨別一個元素。
定義一個好的PIDL顯然是命名空間擴充的中心工作。PIDL應該是資料塊集合,其中的每一個都涉及到從桌面到這個項的路徑中所遇到的檔案夾或子檔案夾。PIDL的結構依賴于你想要檔案夾展示的資料,而且決定怎樣組織PIDL最終是程式員的工作,但是下面推薦的幾點需要考慮:
PIDL應該通過Shell存儲配置設定器(IMalloc接口)來配置設定。這允許探測器釋放它。PIDL不是對象,但是它是一個存儲塊:一旦把它傳遞給探測器,它必須能夠沒有副作用地被釋放。
PIDLs可以被儲存到永久存儲媒體上,和從永久存儲媒體上讀出。例如磁盤檔案。這意味着所有需要的資訊都必須順序被找到。不是指向外部資料的指針,也不是表示外部資料的引用。
由于PIDLs可以是持續的,你可能需要考慮使用簽名和版本号,以便在任何時候總能識别這些PIDL,并保證向後相容。當然,如果對你的應用這并不是主要的,你可以不必這麼做。
PIDL是SHITEMID結構的數組:
typedef struct _SHITEMID
{
USHORT cb;
BYTE abID[1];
} SHITEMID, *LPSHITEMID;
Cb是整個結構的尺寸,包括它自己。abID成員标示一個資料序列的開始,這個序列可以以任何你想要的方式構造。作為例子,考慮下面的PIDLDATA結構:
typedef struct _PIDLDATA
{
TCHAR szSignature[SIGNSIZE];
WORD wVersion;
TCHAR szFileName[MAX_PATH];
BYTE icon[ICONFILESIZE];
} PIDLDATA, *LPPIDLDATA;
這時展示形成檔案名PIDL資料的一種可能的方法——由SHITEMID結構的abID字段指向的一個資料塊,而且有兩件事情需要注意,第一,串包含了全部字元,這也是不能使用指針的理由。TCHAR[]緩沖保證所有内容都順序存儲。第二,我們假設需要存儲圖示,你不能使用HICON作為等價的記憶體存儲塊。相反,你需要連續的形成圖示圖像的所有位元組。
Shell觀察
觀察對象無疑是任何命名空間擴充最值得關注的部分。你所寫的大部分命名空間擴充代碼都在背景工作,靜默的與探測器通訊,從不能明顯地看到它們的活動。
然而,觀察對象則建立和管理視窗——Shell觀察。Shell觀察是一個普通視窗,具有一般的風格和視窗過程。觀察對象就是最終嵌入在探測器右窗框上的視窗,顯示左窗框樹觀察中選中的檔案夾内容。這個觀察對象輸出IShellView接口的方法,以便與Shell觀察一道工作,并且處理任何關系到這個檔案夾使用者接口的事情,消息循環,菜單和工具條拼接。IShellView接口由IOleWindow導出。
IShellView接口
下面是支援IShellView接口所應該實作的函數:
函數 | 描述 |
AddPropertySheetPages() | 允許你添加定制頁面到‘檔案夾選項…’對話框 |
CreateViewWindow() | 建立和傳回嵌入到探測器右窗框中的視窗。它應該是一個無邊框對話框。 |
DestroyViewWindow() | 銷毀上面建立的視窗 |
EnableModeless() | 探測器目前沒有使用,簡單地傳回 E_NOTIMPL。 |
EnableModelessSV() | 探測器目前沒有使用,簡單地傳回 E_NOTIMPL。 |
GetCurrentInfo() | 通過FOLDERSETTINGS結構傳回檔案夾目前設定。 |
GetItemObject() | 為關聯菜單或剪裁闆傳回指向給定項集的接口指針,主要由通用對話框使用。 |
Refresh() | 引起檔案夾内容的重繪 |
SaveViewState() | 儲存觀察狀态 |
SelectItem() | 改變一項或多項的選擇狀态 |
TranslateAccelerator() | 當焦點在擴充上時,轉換任何擊鍵。傳回S_OK以防止探測器再次轉換。 |
UIActivate() | 活動狀态改變時被喚醒——在檔案夾被激活或禁止時。 |
GetWindow() | 傳回觀察的視窗Handle。這個方法來自IOleWindow |
ContextSensitiveHelp() | 檔案夾應該進入或退出關聯感覺的輔助模式,和處理所有不同的消息。這個方法來自IOleWindow |
觀察對象有時給出儲存狀态到永久存儲的機會。在可以這樣做時,探測器就調用SaveViewState()。現在這個過程需要一點技巧,是以,盡管你或許能給出幾個其它的方法來使檔案夾的設定永久保持,我們還是推薦使用流來儲存它。一個指向IStream對象的指針由IShellBrowser接口的GetViewStateStream()方法傳回。且慢,這個接口從哪來的。事實上,這個接口是由探測器實作的,隻是沒有顯式的函數擷取它,一個指向它的指針由Shell觀察在CreateViewWindow()的參數清單中傳遞,因而我們可以儲存它以備将來在擴充中的使用。
對于SaveViewState()函數,需要有下面的代碼做一些操作來輔助:
IStream* pstm;
pSB->GetViewStateStream(STGM_WRITE, &pstm);
pstm->Write(&data, sizeof(data), NULL);
用GetViewStateStream(),你可以握住一個流到你寫狀态設定的地方,如列寬度,圖示,以及任何可以施加于擴充上的操作。要讀回狀态,我們遵循同樣的方法,但是此時打開的觀察流為讀:
IStream* pstm;
pSB->GetViewStateStream(STGM_READ, &pstm);
pstm->Read(&data, sizeof(data), NULL);
這裡的代碼一般是出現在CreateViewWindow(),在構造了Shell觀察視窗之後。沒必要關閉這個流,這直接由探測器完成。
與探測器會話:IShellBrowser接口
在前面的圖表中我們給出了探測器在初始化建立新檔案夾過程中所使用的IShellFolder接口方法。它從IShellFolder中擷取的東西(即,指向IShellView 和其它接口的指針)可以通過IShellBrowser與探測器互動,這是為在探測器與命名空間擴充之間通訊而精确實作的一個接口。
IShellBrowser輸出幾個函數,但是在你的命名空間擴充中使用它主要有兩個目的:
獲得觀察狀态流
與探測器菜單和工具條互動
我們已經讨論了前一個,是以現在讓我們進入到修改菜單和工具條這一步,以便在我們自己的命名空間擴充中添加特殊的項。
修改探測器的菜單
檔案夾,即使是客戶檔案夾也總是一個檔案夾,這就是說,它有一個通常的菜單和工具條,這是任何檔案夾都有的。或者說,如果它決定不改變的話,它有一個通常的菜單和工具條。
檔案夾在獲得焦點時将産生對探測器菜單和工具條的改變,并在失去焦點時删除它們。對于檔案夾,活動狀态由UIActivate()方法來通知:
HRESULT IShellView::UIActivate(UINT uState);
uState變量可以取下述三種可能值之一:
标志 | 描述 |
SVUIA_ACTIVATE_FOCUS | 檔案夾現在有焦點 |
SVUIA_ACTIVATE_NOFOCUS | 檔案夾被選中但是沒有焦點 |
SVUIA_DEACTIVATE | 檔案夾不再有焦點 |
當系統焦點屬于觀察的子元素時,檔案夾有焦點,如果檔案夾僅僅在左窗框被選中,則有這種情況,其中SVUIA_ACTIVATE_NOFOCUS出現。對于每一個不同的活動狀态,都可以有不同的菜單和工具條顯示在探測器的使用者界面中,而且所有這些改變通常都在UIActivate()中完成。
為了改變菜單内——無論是要加一個标記,還是簡單地添加或删除存在的項——你總是需要建立新的,空的頂層菜單。建立新菜單的代碼簡單地是:
hMenu = CreateMenu();
然後,隻需請求Shell用正常的方法填寫它即可,其代碼如下:
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(hMenu, &omw);
OLEMENUGROUPWIDTHS是一個資料結構,形式為六個長整數數組,它在容器和嵌入對象需要共享菜單時起作用(更詳細的資訊請參考VC++或MSDN資料關于OLE in_place編輯)。
基本上,探測器(和OLE容器)的菜單都劃分成六個組,每一組都可以包含許多不同菜單,分組是:
組名 | 位置 | 控制者 |
檔案 | 探測器(容器) | |
編輯 | 1 | 命名空間擴充(對象) |
容器 | 2 | 探測器(容器) |
對象 | 3 | 命名空間擴充(對象) |
視窗 | 4 | 探測器(容器) |
幫助 | 5 | 命名空間擴充(對象) |
在組名與彈出菜單之間不必有一對一的關系,要麼是對名字,要麼對應數字。換句話說,‘檔案’組的第一個菜單可以不同地包含:
單一稱為‘檔案’的菜單
兩個彈出菜單如‘屬性’和‘編輯’
一個‘檔案’彈出菜單和另一個菜單,如‘目錄’
任何其它可能的組合
每一組包含的不同彈出菜單數存儲在OLEMENUGROUPWIDTHS結構中對應的位置。這也被作為菜單組的寬度引用。
通過調用InsertMenusSB(),你可以請求Shell填寫它自己的共享菜單部分,象表中顯示的那樣,容器和對象——此時是探測器和命名空間擴充——各自負責三個組。此時的探測器作如下操作:
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(hMenu, &omw);
菜單如下:
菜單 | 組 |
檔案 | 檔案 |
編輯 | 檔案 |
觀察 | 容器 |
Go | 容器 |
Favorites | 容器 |
工具 | 容器 |
幫助 | 視窗 |
OLEMENUGROUPWIDTHS結構包含{2, 0, 4, 0, 1, 0},而且,可以使用這些值作為偏移來正确地放置任何新的彈出菜單。添加自己的項或彈出菜單,使用傳統的Win32函數,如InsertMenuItem(),DeleteMenu(),或任何可以修改菜單結構的函數。
我們知道AppendMenu(),InsertMenu(),ModifyMenu()和幾個其它的菜單函數是在Win32 API中聲明的老函數,它們仍然被支援并且能很好地工作,但是未來版本的Windows下,使用這些函數的代碼不能保證繼續工作。可以使用新函數InsertMenuItem()來代替它們。
一般來講,主對象應該是一個好的居留者,不應該删除由容器設定的項或彈出菜單。同樣,還應該避免侵入容器為放置菜單到所管理的菜單組而保留的空間。然而,這些規則是應用于OLE容器的,而且,如果探測器定義了一個空的‘編輯’菜單,我們的意見是,在你的檔案夾選中時,沒有理由繼續保留它。另一個可以打破這個規則的情況是在添加客戶‘About’指令時。此時,我們是用自己的項替換了标準項,也就是說删除了由探測器添加的項。
Shell指派唯一的辨別符到它的彈出菜單,以便使你能通過指令而不是位置來做編輯工作。shlobj.h頭檔案定義如下的常量來輔助辨別探測器彈出菜單的指令:
FCIDM_MENU_FILE
FCIDM_MENU_EDIT
FCIDM_MENU_VIEW
FCIDM_MENU_FAVORITES
FCIDM_MENU_TOOLS
FCIDM_MENU_HELP
如上所示,缺少了‘Go’菜單對應的常量。如果察看shlobj.h頭檔案,則會發現另一個類似的常量,但是它并不關聯于命名空間擴充修改菜單。
被添加項的ID必須在0x0000 到0x7fff之間,這個限制封裝在常量FCIDM_SHVIEWFIRST和FCIDM_SHVIEWLAST之中。一旦你根據需要改變了菜單,就必須使用下面調用儲存它:
m_pShellBrowser->SetMenuSB(hMenu, NULL, hwndView);
奇怪的是SetMenuSB()函數在說明中隻有兩個變量:
HRESULT SetMenuSB(HMENU hmenuShared, HOLEMENU holemenuReserved);
而它實際要求三個變量:
HRESULT SetMenuSB(HMENU hmenuShared, // 共享菜單
HOLEMENU holemenuReserved, //探測器目前忽略
HWND hwndActiveObject // 觀察視窗的 Handle
);
頭一個變量是探測器與命名空間擴充共享的菜單,第二個是不考慮的,最後一個是檔案夾展示視窗的Handle。為了了解為什麼目前忽略第二個變量。我們需要暫時離題來讨論:在容器和對象之間共享菜單時,OLE in-place編輯是怎樣工作的。
IShellBrowser的關于菜單和工具條操作的方法來自于IOleInPlaceFrame所采用的類似方法,他們是由OLE容器實作的接口方法。從一方面看,探測器本身對命名空間擴充是一個特殊的容器,在修改了菜單之後,典型的宿主于OLE容器的對象将需要填寫它自己的OLEMENUGROUPWIDTHS結構部分,然後建立‘OLE菜單描述符’。事實上,有一個函數确切地做這個工作,其原型如下:
HOLEMENU OleCreateMenuDescriptor(HMENU hmenuShared, // 組合菜單
LPOLEMENUGROUPWIDTHS lpMenuWidths // 更新的OLEMENUGROUPWIDTHS
)
其次,它傳遞這個HOLEMENU到SetMenu(),這是一個非常類似于SetMenuSB()的方法,其原型為:
HRESULT SetMenu(HMENU hmenuShared, HOLEMENU holemenu);
在這個函數内部,容器最終調用OleSetMenuDescriptor(),它負責設定由菜單生成消息的派遣代碼。實際這個函數安裝了一個鈎子,來感覺菜單消息,然後派遣它們到正确的視窗——是容器,還是對象的視窗。為了了解給定消息的目标是哪一個視窗,這個鈎子簡單地察看菜單生成的位置,它通過比較位置和由HOLEMENU引用的OLEMENUGROUPWIDTHS結構中的值解析疑惑。現在,探測器使用不同的邏輯來指派消息,而且既不需要HOLEMENU,也完全不需要客戶充填OLEMENUGROUPWIDTHS結構。在調用SetMenuSB()時,探測器設定鈎子來解釋消息,而且消息都被指派到活動的觀察視窗(hwndActiveObject參數),這個視窗是由它的ID來辨別的,而不是通過菜機關置辨別的。在活動狀态不同于SVUIA_DEACTIVATE時,菜單将産生所有這些改變。在使用者解除檔案夾的活動狀态時,你必須恢複前一個狀态,如此,隻需通過調用IShellBrowser::RemoveMenusSB()來删除菜單即可:
m_pShellBrowser->RemoveMenusSB(hMenu);
DestroyMenu(hMenu);
修改探測器工具條
要處理工具條,首先需要知道它的視窗Handle,這是IShellBrowser接口提供的另一個服務——需要調用GetControlWindow()函數,它正好傳回HWND。然而,資料說明禁止直接發送消息到這個視窗,是以為了向前相容,你應該使用SendControlMsg(),這是IShellBrowser的另一個方法:
HRESULT IShellBrowser::SendControlMsg(UINT id,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
LRESULT* pret);
它看上去類似于普通的SendMessage()函數,有兩點不同:id變量辨別你正在選擇的控件(工具條或狀态條),pret 則取得你發送的消息的傳回值。使用FCW_TOOLBAR常量來選擇工具條。
參考VC++ 或MSDN資料獲得關于結構和消息的詳細解釋。
此時,你可以發送消息來添加任何你希望的按鈕。然而添加按鈕到工具條并不是一項容易的工作。工具條按鈕有一個位圖,是以首先必須在工具條中注冊一個新的位圖。下面是說明操作過程的代碼段:
TBADDBITMAP tbab;
tbab.hInst = g_hInstance; // 設定包含位圖的子產品
tbab.nID = IDB_TOOLBAR; // 子產品資源中的位圖 ID
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_ADDBITMAP,
1, reinterpret_cast<LPARAM>(&tbab), &lNewIndex);
TBADDBITMAP結構隻包含兩個成員來辨別一個位圖。一是在資源中包含位圖的子產品,另一個是适當的ID,你使用TB_ADDBITMAP消息傳遞這個結構到工具條。這個消息可以實際接收一個結構數組,是以消息的wParam變量(上面代碼中為1)表示了數組尺寸,而lParam則指向第一個元素。lNewIndex是包含消息傳回值的緩沖,這是重要的,因為它是所生成圖像在全部工具條位圖中的索引。形成工具條的所有位圖都存儲在單一位圖中,逐個小圖像連續存放。
添加文字标簽也使用相同的技術。(進入探測器工具條的按鈕也需要文字标簽)
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_ADDSTRING,
NULL, reinterpret_cast<LPARAM>(szLabel), &lNewString);
此時所涉及到的消息是TB_ADDSTRING,lParam變量指向串,lNewString将包含輔助工具條識别文字标簽的索引。注冊了新的位圖和文字串到工具條後,就可以聲明TBBUTTON結構來展示按鈕了:
TBBUTTON tbb;
ZeroMemory(&tbb, sizeof(TBBUTTON));
tbb.iBitmap = lNewIndex;
tbb.iCommand = IDM_FILE_DOSOMETHING;
tbb.iString = lNewString;
tbb.fsState = TBSTATE_ENABLED;
tbb.fsStyle = TBSTYLE_BUTTON;
最後發送設定新按鈕的消息:
m_pShellBrowser->SetToolbarItems(&tbb, 1, FCT_MERGE);
如果你有勇氣,可以使用底層方法直接發送消息到工具條,然而應該再次指出微軟阻止使用這項技術:
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_INSERTBUTTON,
0, reinterpret_cast<LPARAM>(&tbb), NULL);
這行代碼添加新按鈕到工具條的開始位置,作為第一個按鈕。這個方法實際上比我們給出的任何添加按鈕的方法都更有效。使用SetToolbarItems()添加按鈕總是加在工具條的末端。
即使沒有顯式的資料說明,我們也建議你應該在檔案夾失去焦點時删除所有添加的按鈕。唯一的選擇是使用SendControlMsg(),無論添加按鈕使用的技術如何:
TBBUTTON tbb;
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_GETBUTTON,
0, reinterpret_cast<LPARAM>(&tbb), NULL);
if(tbb.idCommand == IDM_FILE_RUN)
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_DELETEBUTTON, 0, TRUE, NULL);
在删除按鈕之前一定要確定它是你想要删除的。檢查指令ID是一項可靠的技術。
通路探測器狀态條
上面工具條中我們使用的接口也可以用于狀态條來設定客戶文字,調用IShellBrowser::SendControlMsg()時辨別狀态條的ID為FCW_STATUS。此時,你可以完全自由地發送消息,和以你的方式格式化狀态條。然而,如果你隻是簡單地想要某些文字顯示的話,我們建議你使用IShellBrowser的另一個輔助函數,SetStatusTextSB():
m_pShellBrowser->SetStatusTextSB(wszText);
這個函數的唯一缺點是(如果開發的是基于ANSI的軟體)它需要Unicode串。你必須自己站換。
附加接口
在使用探測器期間,你可以對每一個項激活關聯菜單,拖拽它們,甚至拷貝它們到剪裁闆。這些并不是探測器的内建行為,而是由檔案夾本身提供的特征。更精确一點,它是由管理檔案夾外觀和行為的命名空間擴充提供的。誰也沒有檔案夾本身更了解其中項的實際表現和處理方式。
在檔案夾執行封裝資料的特殊活動或準備菜單期間,觸發者仍然是探測器。它感覺到使用者的活動,請求命名空間擴充提供資料拷貝到剪裁闆或拖拽。
探測器的右窗框整個由觀察對象繪制,但是命名空間擴充沒有控件在左窗框上出現。不過即使在樹觀察中使用者也可以喚醒關聯菜單或打開子樹,來顯示給定檔案夾中的子檔案夾清單。這個關聯菜單是誰提供的,還有填充樹觀察的圖示?這始終是命名空間擴充在探測器請求上所作的活動。這一點也不奇怪,命名空間擴充通過實作幾個附加的COM接口來做這個工作:IContextMenu,IDataObject 和IExtractIcon。它們的功能我們在相關的章節中都已經解釋過了。如果某個接口缺失了,相關的功能就是不可用的。
取得附加接口指針
在上一章,我們看過幾個Shell關聯菜單的例子,是以應該還有怎樣實作IContextMenu接口的基本概念。在命名空間擴充内實作IContextMenu基本上與Shell擴充一樣——改變的隻是初始化過程。當使用者右擊探測器樹觀察時,Shell努力獲得指向檔案夾實作的IContextMenu接口指針。如果傳回了可用的東西,關聯菜單将顯示,否則操作被拒絕。探測器獲得指向IContextMenu接口指針的方法是IShellFolder::GetUIObjectOf()。下面是其典型的實作:
STDMETHODIMP CShellFolder::GetUIObjectOf(HWND hwndOwner, UINT uCount,
LPCITEMIDLIST* pPidl, REFIID riid, LPUINT puReserved, LPVOID* ppvReturn)
{
// 清除傳回資料的緩沖
*ppvReturn = NULL;
// 如果接口請求的PIDL數>1 則失敗
if(uCount != 1)
return E_FAIL;
// 檢查實作的附加接口的 riid
// IExtractIcon
if(IsEqualIID(riid, IID_IExtractIcon))
{
CExtractIcon* pei;
pei = new CExtractIcon(pPidl[0]); // pPidl 數組的第一項
if(pei)
{
pei->AddRef(); // 增加引用計數
pei->QueryInterface(riid, ppvReturn); // QI 提示增加引用計數
pei->Release(); // 減少引用計數
return S_OK;
}
return E_OUTOFMEMORY;
}
// IContextMenu
if(IsEqualIID(riid, IID_IContextMenu))
{
CContextMenu* pcm;
pcm = new CContextMenu(pPidl[0]);
if(pcm)
{
pcm->AddRef();
pcm->QueryInterface(riid, ppvReturn);
pcm->Release();
return S_OK;
}
return E_OUTOFMEMORY;
}
// 檢查其它可能的接口...
return E_NOINTERFACE;
}
這個代碼段來自後面将要讨論的例子,其中CShellFolder是實作IShellFolder接口的 C++ 類。同樣CExtractIcon 和CContextMenu 也實作了IExtractIcon 和 IContextMenu。現在讓我們檢視一下這些方法的原型。hwndOwner是任何顯示對話框或視窗的父視窗Handle。這個方法接收一個PIDLs數組(pPisl),其尺寸在uCount中傳遞。Riid 請求的接口适用于這個數組的所有元素。
有一些接口并不能在多重PIDLs上同時操作——例如,關聯菜單,總是引用一個項,圖示也是如此。相反,IDataObject可以用于拷貝到剪裁闆,或拖拽一個項集。在上面的例子中我們僅僅實作了IContextMenu 和 IExtractIcon,是以首先檢查傳遞的PIDLs數。
在命名空間擴充中,我們并不需要從IShellExtInit 或 IPersistFile中導出實作Shell接口的類。命名空間擴充内接口使用PIDLs清楚地辨別所操作的項。PIDL是Shell傳遞給GetUIObjectOf()的一個變量,是以最合理的是由一個類構造器來接收PIDL。使用這種方式,就可以初始化你的關聯菜單,令其知道它所引用的是什麼項。
關聯菜單
在IContextMenu接口的三個函數中,僅需要實作InvokeCommand()和QueryContextMenu(),不必實作GetCommandString()。這些方法的實作遵循前一章給出的規則。
客戶圖示
如果你想要對探測器窗框中檔案夾項的實際顯示圖示進行控制,就需要實作IExtractIcon接口。如第15章解釋的,你可以使用兩個方法傳遞圖示到Shell:GetIconLocation()和Extract(),它倆是互相排斥的,一個調用成功,另一個就不能被調用。GetIconLocation()期望傳回抽取檔案的路徑名和圖示索引,實際抽取則由Shell執行。相反,Extract()做抽取操作,傳回大圖示和小圖示的HICON。
拖拽操作
如果想要支援檔案夾項的拖拽操作,你需要實作适當的接口。尤其是需要IDropTarget,以便确定項目落下時需要做什麼。
拷貝到剪裁闆
拷貝到剪裁闆與包裝拖拽資料都需要通過IDataObject接口傳遞資料。這是一個通用接口,提供在應用之間跨系統移動的各種格式資料,雖然Windows自己管理實際資料的存儲。你所要做的就是給出你自己的格式和資料。
檔案夾概念
寫一個命名空間擴充就是你有某些想要使用探測器的文檔為中心概念和層次邏輯展示的内容。也就是說你必須開始以檔案夾,子檔案夾和項的概念考慮你的内容。有時,這樣更容易和直接。另一些場合則需要花一點時間進行整理。也有一些情況是根本不可能這樣做的。檔案夾的概念——在你的腦海中是清晰的,檔案夾關聯了什麼——是開發命名空間擴充的中心。
一個命名空間擴充是一個主檔案夾,即一種根目錄。它的内容可能被劃分成其它子檔案夾和項。最簡單的情況是沒有檔案夾和要顯示的項,此時,你隻是插入應用到探測器的架構中。後面我們将更進一步說明這一點。
你必須對探測器聲明你的檔案夾是什麼,以及它們是否有子檔案夾。一定要保證精确地做這個操作,因為它可能會影響到 Shell處理擴充的方式。實際上,所有你聲明成檔案夾的項都将作為父檔案夾的子節點顯示在樹觀察中。如果你聲明一個檔案夾有子檔案夾,探測器則使它成為樹觀察中的可擴充節點。
探測器通過調用IShellFolder::EnumObjects()來請求擴充枚舉它的内容,這個函數在每次探測器需要枚舉檔案夾部分時都被調用,而不是隻調用一次來獲得枚舉器對象的指針。下面是它的典型實作:
STDMETHODIMP CShellFolder::EnumObjects(HWND hwndOwner, DWORD dwFlags,
IEnumIDList** ppEnumIDList)
{
*ppEnumIDList = NULL;
// m_pidl 是 CShellFolder 的成員,存儲這個檔案夾的 PIDL 。必須在類的構造其中填寫它。
*ppEnumIDList = new CEnumIDList(m_pidl, dwFlags);
if(*ppEnumIDList == NULL)
return hr;
return S_OK;
}
在幾乎所有IShellFolder的實作中都定義了m_pidl資料成員,以儲存檔案夾的PIDL。(Shell 在通過IPersistFolder綁定這個檔案夾時傳遞它的PIDL)
這裡重要的是EnumObjects()接收一個DWORD來表示Shell想要枚舉器提供那種項。換言之,dwFlags工作就象濾波器,使Shell的請求施加于任何IEnumIDList::Next()的傳回。
當然,最終由你來決定對于你的客戶檔案夾這個标志是否有意義。沒有任何東西阻止你簡化忽略這個标志,隻要你甘願冒這個風險。
檔案夾屬性
dwFlags可以設定的值來自SHCONTF枚舉類型,其定義如下:
typedef enum tagSHCONTF
{
SHCONTF_FOLDERS = 32,
SHCONTF_NONFOLDERS = 64,
SHCONTF_INCLUDEHIDDEN = 128,
} SHCONTF;
它告訴你是否它想要檔案夾,檔案夾和項,以及是否包含隐藏項。在正常情況下,Shell不要求檔案夾僅僅枚舉項,但是如果在你的設想中它是合理的,在你認為是适當的時候可以隻傳回項。要聲明我們的項為檔案夾所要做的是什麼呢?我們正好要回答什麼時候Shell請求給定項的屬性。這是通過IShellFolder::GetAttributesOf()方法來完成的,下面的清單說明了它的基本實作:
STDMETHODIMP CShellFolder::GetAttributesOf(UINT uCount, LPCITEMIDLIST aPidls[],
LPDWORD pdwAttribs)
{
*pdwAttribs = 0xFFFFFFFF;
for(UINT i = 0 ; i < uCount ; i++)
{
DWORD dwAttribs = 0;
if(IsThisAFolder(aPidls[i]))
dwAttribs |= SFGAO_FOLDER;
if(HasSubFolders(aPidls[i]))
dwAttribs |= SFGAO_HASSUBFOLDER;
*pdwAttribs &= dwAttribs;
}
return S_OK;
}
當然IsThisAFolder()和HasSubFolders()是假象的函數,你應該使用自己的例程替換它們——我們放置它們在這裡隻是想要說明GetAttributesOf()應該有的代碼架構。使用PIDLs數組調用這個函數,你應該指定它們所有的全部屬性。
這裡所看到的屬性并不是你所能唯一設定的,在第4章中有完整的 SFGAO_XXX 常量清單。
命名空間擴充的風格
命名空間擴充不隻是代碼,還相關于安裝期間實際存儲在系統資料庫中的内容,它可能以稍微不同的方式展示自己。是以你或許會聽到過兩種類型的命名空間擴充:根和非根。
二者之間的差異不在代碼,而在于系統資料庫條目——要實作的接口和所遵循的行為都是相同的。所不同的是容留它的探測器觀察。在這一節我們詳細讨論根與非根擴充,并确定什麼情況應該做出選擇。
根擴充
基本上,根擴充是一個以自身為根的檔案夾,就是,你不能進一步浏覽到它的上層,或跳到同等深度的并行節點。根擴充隻顯示它自己的子樹,并且内容完全與探測器的其餘命名空間隔絕。具體例子看下面這個截圖:
它顯示了一個視窗,當你在任務條的‘開始’菜單上要改變設定時這個視窗出現。開始菜單節點是這棵樹的根,像上一層的按鈕被禁止,是以,你不能向整個空間的更高層移動。
這個圖沒有顯示客戶擴充,而是簡單地顯示根觀察。根命名空間擴充是具有根觀察的客戶檔案夾。
非根擴充
非根擴充實際是相對于根擴充而言的,它們沒有固定的根,允許你在整個探測器層次上浏覽。非根擴充完美而确切地內建到Shell的命名空間中。注意,同一個命名空間擴充可能以兩種方式通路和使用,這就提供了對前面說明的驗證:‘根’和‘非根’是施加到觀察的特征,而不是檔案夾的特征,無論它是不是客戶的檔案夾。
根與非根
現在我們來概括某些觀點。你有一個客戶檔案夾——即,一個命名空間擴充。你有機會使它可通過兩個可能的觀察來通路:根和非根。前者,觀察顯示檔案夾作為單獨對象,你看到一個受限的Shell命名空間部分。後者,探測器觀察包含了所有檔案夾,也包括我們的客戶檔案夾,是以你可以在檔案夾中前後移動。
我們可以建立快捷方式以兩種觀察打開同一個檔案夾。象在第11章中解釋的那樣,我們要做的所有事情就是在探測器的指令行中指定/root開關:
explorer /root, ::{clsid}
這裡的例子表示打開一個由CLSID表示的根命名空間擴充。通過 /e 開關可以要求顯示作窗框的樹觀察。
explorer /e, ::{clsid}
上面的指令使用指定選中的和打開的檔案夾打開傳統的探測器觀察。這是非根觀察。
什麼時候做出選擇
從代碼的觀點我們在重複一次,在編寫根擴充與非根擴充之間絕對沒有差别。事實上,這個特征應該更确切地施加于觀察而不是擴充本身。但是,什麼時候使用根或非根觀察來使我們的命名空間擴充可通路呢?我們認為這個問題是存在争論的,但是,一般來講,隻有檔案夾的内容确實獨立,幾乎和應用一樣時才使用根觀察。
不管你寫了多少擴充,探測器還是基于檔案管理器的,是以,對于不能在檔案系統元素上工作的任何擴充你都應該考慮使用根觀察,相反,當希望以客戶的方式表示相關的檔案和目錄資訊時,非根觀察是一個好的選擇。例子就是‘Temporary Internet Files’檔案夾,它聚集四個隐藏的目錄,所有檔案都在Internet會話期間靜默的下載下傳。當你打開這個檔案夾時,你并不能看到引用的四個子檔案夾,而僅僅是檔案。
關于根與非根的讨論直接把我們帶到了開發命名空間擴充的另一個重點。這與某些其它方面不同,它對擴充的工作和使用有決定性的影響。
連接配接點
編寫命名空間擴充僅僅是過程的一半,你知道我們還需要鍵入某些系統資料庫資訊。然而,有時我們需要做得比這還要多。依據我們在系統資料庫中的設定,和擴充實際的活動,我們可能需要一個稱之為連接配接點這樣的東西。
在堅硬的外殼下,連接配接點是一種方法,通過這種方法我們能夠連接配接我們的檔案夾到Shell的命名空間。這實際并不是新概念——在我們講到Shell擴充時連接配接點就已經出現。但是在第15章中我們并沒有提到它們,因為它們是通過為Shell擴充提供的系統資料庫資訊自動處理的。對于命名空間擴充,這有點不同。
首先,命名空間擴充是程序内伺服器,是以它要求适當地注冊在HKEY_CLASSES_ROOT\CLSID鍵下,其次,它可能需要某些特殊的注冊資訊(後面我們将精确地讨論這一點)它們服務于檔案夾的行為和顯示設定。
仍然要明顯定義的是命名空間擴充怎樣與Shell連接配接。換言之,檔案夾應該定位在Shell層次的那個地方。對于Shell擴充并沒有這個問題(例如關聯菜單處理器),因為Shell擴充是動态對象,它僅在需要的時候才獲得調用,并且當它的引用計數傳回到0後幾秒鐘之内就被解除安裝。
相反,命名空間擴充是一個檔案夾,而檔案夾應該在Shell中有一個位置。這個位置就是連接配接點。從另一個觀點上看,我們可以說連接配接點就是通路命名空間擴充的方式。有四種方法來定位連接配接點:
用檔案類型連接配接命名空間擴充(如果可用)
使用具有非常特殊内容的目錄
使用具有特殊名字的目錄
用一個已存在的命名空間連接配接它
下面讓我們仔細的測試一下每一種選擇。
使用檔案類型
可能有點不太尋常(至少在我們的選擇中),你可以考慮寫一個命名空間擴充,使使用者可以浏覽你的文檔内容。此時,你的擴充将綁定到檔案類型,并且連接配接到關聯菜單預設項。這說明命名空間擴充與Shell擴充實際上并沒有多大差别。
自私地說,如果你有一個文檔類型允許自身内容被浏覽,最好是定義外部觀察器,就象在第14章中我們對元檔案的操作那樣。使用外部觀察器的明顯優點是你省下了所有COM技巧和命名空間擴充的架構。缺點是建立了另一個程序,而命名空間擴充是運作在探測器位址空間上的。(這也可以看作是優點,因為不同的程序能更好地抗拒沖突)
使用檔案類型作為連接配接點,你需要在文檔類的關聯菜單上建立新動詞:
HKEY_CLASSES_ROOT
\YourDocument
\ShellEx
\NewVerb
\Command
在這個樹中YourDocument和NewVerb是兩個定制的鍵。如果你想要添加命名空間擴充來周遊HTML檔案的内容,系統資料庫條目應該是:
HKEY_CLASSES_ROOT
\htmfile
\ShellEx
\Browse
\Command
此時我們選擇一個‘浏覽’動詞,它将把‘浏覽’自動加到.htm的關聯菜單中(我們在第14章看到了這個)。為了連接配接命名空間擴充與HTML檔案,指令行應該是:
explorer /root, {CLSID}, %1
有某些檔案可以作為不同類型資訊的容器來工作,而且可以把這些考慮成檔案夾和子檔案夾。在HTML檔案中可以找到這樣的例子(包含對象,圖像,表等的集合),如果它們是目錄,你就可以浏覽。此時有比命名空間擴充更好的辦法嗎?
在這種情況下,我們必然要使用根擴充,因為探測器不支援在樹觀察中浏覽檔案内容。
使用目錄
正常情況,使用目錄連接配接命名空間擴充到Shell。這個概念是非常簡單的,因為命名空間擴充是客戶檔案夾,你必須用平常的名字建立一個普通檔案夾,并且把它與你提供非标準行為的擴充聯系到一起。有兩個等價的方法來完成這一步。
desktop.ini檔案
第一項技術要求你在可能的地方建立新目錄,并給它一個你希望的名字。然後使它為隻讀并建立一個隐藏檔案desktop.ini。這個檔案的典型内容為:
[.ShellClassInfo]
CLSID={CLSID}
這就告訴探測器為需要顯示的檔案夾資訊引用你指定的CLSID。你還可以通過設定下面路徑下的CLSID預設鍵來改變這個檔案夾的顯示名:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
同樣,要設定客戶圖示,隻需在同一路徑下添加DefaultIcon鍵即可:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
\DefaultIcon
這個鍵的預設值将定位所使用圖示的位置和索引,一般的文法是:
"filename, index"
特殊檔案夾名
一個更簡單的(可能很少有人知道的)技術是建立一個檔案夾并給它一個特殊的名字。沒有其它操作。這個檔案夾被自動建立,并具有隻讀屬性,而且使用你設定在系統資料庫中的圖示和标題來顯示,系統資料庫設定如上所述。檔案夾的名字應該有如下形式:
YourFolderName.{CLSID}
例如
SpecialName.{04051965-0fcc-11ce-bcb0-b3fd0e253841}
依附已存在的命名空間
Windows Shell提供了一個命名空間擴充聚集,其中有‘我的計算機’,‘網路上的芳鄰’,以及幾個其它擴充。這就可以使你把自己的命名空間擴充放到這些已存在的特殊檔案夾之下。尤其是你可以把它直接加到‘我的計算機’,或‘桌面’命名空間下。這樣做可以自動地連接配接擴充到Shell,你不必作任何其它的動作。下面是添加命名空間擴充到‘我的計算機’節點的例子:
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\MyComputer
\NameSpace
\{CLSID}
{CLSID}的預設值應該指向你希望探測器顯示的串。其它可以依附的命名空間是:
桌面(Desktop)
網路上的芳鄰(NetworkNeighborhood)
Internet探測器(Internet)
通過簡單地用括号裡的文字置換上面路徑中的‘MyComputer’條目,你就可以移動你的擴充到期望的命名空間。
命名空間擴充能做些什麼
到目前為止,我們讨論了命名空間擴充的體系結構,我們還揭示了它的工作過程,以及一些基本概念。我們也接觸了安裝方面的熱點課題。現在我們開始考慮這項技術的實際應用。如果你檢視PC中的目錄,你将看到許多具有定制圖示的檔案夾。有‘訂閱’,‘下載下傳程式檔案’,‘臨時Internat檔案’,‘任務排程’,‘通道’,‘軟體更新’… 等等。基本上,每當有某些資訊可以邏輯上表示為嵌套檔案夾形式顯示時,Windows都給出命名空間擴充。這些要喚醒的資訊必須收集到單一的主檔案夾之中,并且必須關聯到系統。如果是基于檔案的就更好了。
這正是關于命名空間擴充的一種思考方式。另一個則集中于應用。你可以考慮在Shell中建立你自己的檔案夾,并為團隊的每一個應用保留一個子檔案夾。在這些子檔案夾中,你可以駐留整個應用,或關于它的資訊,或(更簡單)隻是一個Internet快捷方式。
設計我們的命名空間擴充
下面這幾點概述了這一章的前一部分的内容,我們需要對一定數量的問題給出一緻的回答。我們想要加一個節點到探測器,使我們能夠層次浏覽目前打開的視窗。如果你了解Spy++ 這個随VC++ 給出的實用程式,很容易想象我們所瞄準的目标:基本上我們想要在Windows Shell上實作大多數Spy++的功能。通過擴充,如視窗觀察節點,我們希望獲得所有頂層視窗的完整清單。通過展開這些節點,我們能夠找出上層視窗的所有關聯視窗。
我們需要确定的是:
是否這個應用包含了‘檔案夾’的概念
怎樣建造PIDL
怎樣枚舉項
怎樣表示資訊給使用者
要提供哪些附加的功能
在這一章的剩餘部分,我們将緻力于解決這些問題。
此時的檔案夾是什麼
在視窗層次中,我們正好有一種項:視窗。這不同于觀察系統資料庫的命名空間擴充的情形。那時的檔案夾是系統資料庫的鍵,而項是系統資料庫鍵的值。
視窗觀察擴充僅僅由視窗組成。如果給定視窗有子視窗,則它可以被考慮為檔案夾。什麼是子檔案夾呢?子檔案夾是一個具有父視窗的視窗,并且至少有一個子視窗。
設計定制的PIDL
在命名空間擴充開發階段設計PIDL是非常重要的。在這裡的特殊情況下,我們是相當幸運的。我們不需要連接配接資料部件來獲得視窗的唯一辨別,因為我們總能得到HWND。我們将直接使用HWND作為我們的PIDL,而且這是絕對可靠的,我們能清晰地識别任何檔案夾項。
怎樣建構Windows枚舉器
命名空間擴充的另一個中心課題是枚舉器對象,它有傳回檔案夾或子檔案夾包含的各個項的任務。我們還是幸運的,因為視窗是一個系統部件,SDK對它提供了大量的支援。要枚舉視窗,我們隻需要調用EnumChildWindows(),并且把結果儲存在自己的資料結構中即可。
設計觀察
對于程式員,關于視窗的關鍵資訊是它的HWND,它的标題,以及視窗類名。是以我們建立的這個觀察應該允許你同時看到所有這些資訊。一個報告型清單觀察的顯示是最好的選擇,我們給出四個列:
訓示是否視窗有子視窗
視窗的HWND
視窗所屬的類名字
視窗的目前标題
為了友善觀察,我們采用不同的圖示來反映視窗是否有子視窗——這是差別檔案夾視窗和項視窗的一種方法。進一步加入一些排序能力是有幫助的。
我們還想要檔案夾通過關聯菜單提供關于視窗的資訊,這将要求我們實作IContextMenu接口。
實作我們的命名空間擴充
為了建立這個命名空間擴充,我們使用微軟提供的‘系統資料庫觀察’擴充作為基礎代碼結構。系統資料庫觀察擴充顯示如圖:
這個關于RegView例子源碼添加一個新檔案夾到探測器,是與Internet 用戶端SDK已同釋出的,在VC++ 上也有。你可以在Samples\SDK Samples\Windows UI Samples\Shell Samples\RegView下找到它。我們建議你找出這段代碼,以便能夠更好的了解這裡的讨論。
系統資料庫觀察與Windows觀察的共有屬性
前面我們提到使用微軟的示例源代碼作為基礎代碼是因為它實際是十分現實的例子。它向你展示:
怎樣設計一個将自己插入到探測器命名空間的相當複雜的命名空間擴充
怎樣編碼和管理PIDL,以及把實際資料嵌入其中
怎樣處理群組織子檔案夾
怎樣添加附屬特征如修改菜單和不同的圖示
怎樣把它放到桌面上,并儲存附加的和有幫助的資訊到系統資料庫
我們維護這個代碼結構,并試圖使它與另一種資料類型,不同的特征一起工作。這個例子采用純C++代碼,是以我們維護它。所有COM環境初始也是純C++的。類似地我們也保持PIDL的管理代碼——在單一管理類中封裝每一件東西是一個較好的選擇。我們隻改變資料格式,并且采用某些類成員。
兩個擴充(微軟的和我們的)都采用了清單觀察作為視窗顯示檔案夾内容然而我們的更簡單一點。它不支援多重觀察(大圖示,小圖示,和清單),而僅支援報告觀察。相反,它提供排序能力和某些增強的清單觀察使用者界面(全行選擇,自動跟蹤,列拖拽)。
另外,兩個擴充都改變了探測器菜單并支援不同項顯示不同圖示。此外,視窗觀察實作了左右兩個窗框的關聯菜單。談到使用者界面特征,我們應該添加我們的,從公共對話框可見的屬性,以及當桌面上滑鼠在其上盤旋時提示的工具标簽。
除了維護C++架構群組織PIDL相關代碼之外,其餘的——也是最大一部分代碼——有一個标準的形式。它所實作的活動不能明顯以根本不同的方式完成,是以,在編寫命名空間擴充時它适合作為一個子產品。
視窗觀察項目
這個項目由下面主要的類組成,所有這些類都在系統資料庫觀察項目中存在:
類 | 接口 | 描述 |
CShellFolder | IShellFolder, IPersistFolder | 定義檔案夾管理器的行為,這是一個實作橋接探測器與擴充的子產品 |
CEnumIDList | IEnumIDList | 枚舉在觀察部分的視窗 |
CShellView | IShellView | 提供一個占據探測器右窗框的觀察 |
CExtractIcon | IExtractIcon | 傳回探測器使用的圖示 |
CContextMenu | IContextMenu | 傳回探測器為關聯菜單使用的菜單項 |
除了這些實作COM接口所必需的類以外,我們的項目還包含另一個重要的類,這個類提供管理PIDL的主要函數:CPidlMgr。
PIDL管理類
我們讨論過,這個命名空間擴充使用視窗Handle作為PIDLs,但是我們仍然需要代碼層來封裝這個HWND,以及提供程式設計接口來遵守PIDL規格和Shell的期望進行協調。
為了完成設計PIDL的所有任務,我們需要定義一個PIDL管理類。任何需要處理PIDL的類都将建立這個對象的執行個體。
#ifndef PIDLMGR_H
#define PIDLMGR_H
#include <windows.h>
#include <shlobj.h>
// PIDL的資料結構
struct PIDLDATA
{
// 如果要向後相容,在這裡添加簽名和版本号,對于你這是實際結果,好藥添加其它資料,要求辨別
//你的檔案夾元素。
HWND hwnd;
};
typedef PIDLDATA* LPPIDLDATA;
extern HINSTANCE g_hInst;
extern UINT g_DllRefCount;
/*---------------------------------------------------------------*/
// CPidlMgr class definition
/*---------------------------------------------------------------*/
class CPidlMgr
{
public:
CPidlMgr();
~CPidlMgr();
LPITEMIDLIST Create(HWND);
LPITEMIDLIST Copy(LPCITEMIDLIST);
void Delete(LPITEMIDLIST);
UINT GetSize(LPCITEMIDLIST);
LPITEMIDLIST GetNextItem(LPCITEMIDLIST);
LPITEMIDLIST GetLastItem(LPCITEMIDLIST);
BOOL HasChildren(HWND);
BOOL HasChildrenOfChildren(HWND);
HWND GetData(LPCITEMIDLIST);
DWORD GetPidlPath(LPCITEMIDLIST, LPTSTR);
private:
LPMALLOC m_pMalloc;
HWND GetDataPointer(LPCITEMIDLIST);
static BOOL CALLBACK WindowHasChildren(HWND, LPARAM);
};
typedef CPidlMgr* LPPIDLMGR;
#endif // PIDLMGR_H
CPidlMgr類定義了建立,删除,拷貝,和浏覽PIDL的方法,進一步,這個類還包含資料成員來存儲對由SHGetMalloc()傳回的Shell配置設定器對象的引用。這個類中的關鍵函數是:
Create(), 建構一個新的PIDL
HasChildren(), 确定是否視窗是一個檔案夾
GetData(), 分解PIDL,以抽取有用的資訊
GetPidlPath(), 傳回PIDL的顯示名
其它大部分函數相對于上面四個則是次要的。下面我們将更詳細的檢測這些方法。
建立PIDL
用HWND聯系PIDL并不是說我們可以在任何需要PIDL的地方簡單的使用HWND。PIDL是一個結構,必須輸出标準接口使探測器能夠浏覽它,無論它内部包含什麼。因而,HWND隻是PIDL的内容,最終建立的PIDL則是建立封裝這個Handle的包裝。正如我們早期解釋的,PIDL是一個指向SHITEMID變量清單的指針。一開始我們就定義了一個PIDLDATA資料結構,這是由SHITEMID結構的abID成員指向的東西:
struct PIDLDATA
{
HWND hwnd;
};
typedef PIDLDATA* LPPIDLDATA;
這是一般的處理方法:建立客戶結構并用需要在實際包含它的檔案夾内辨別項的任何資料充填。對于單個PIDL,沒有必要是全程唯一的,象HWND那樣。重要的是在子檔案夾内的每一個PIDL是唯一的。全程唯一性通過連接配接各個PIDLs從桌面到這個項形成路徑來達到。換句話說,這個工作十分類似于檔案和目錄的工作。可以有兩個檔案具有相同的名字在不同的目錄下,或具有相同的路徑在不同的驅動器上,此時仍能确定它們為不同的對象。
在這個特殊的例子中,我們不需要使用更多的HWND來識别檔案夾項。在其它情況下,你可能需要更多的資訊,然而,隻需要在PIDLDATA結構中加入新的字段即可。
在建立新PIDL時,你必須通過IMalloc接口配置設定足夠的記憶體。這使 Shell 釋放這個記憶體成為可能。IMalloc接口是由SHGetMalloc()傳回的,這個調用在CPidlMgr類的構造中進行。實際要配置設定的存儲量必須等于PIDL本身的尺寸加上一個空結構的尺寸。下面是正确的尺寸定義:
USHORT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);
正如我們所看到地,它是由ITEMIDLIST尺寸加上表示項的資料尺寸給出的。此外還不能忘了最後的NULL,ITEMIDLIST使Shell知道這是一個完整的連結清單:
LPITEMIDLIST CPidlMgr::Create(HWND hwnd)
{
// PIDL的完整尺寸,包括 SHITEMID
USHORT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);
// 配置設定存儲還要包括最後的空結構ITEMIDLIST,注意必須使用 IMalloc 。
LPITEMIDLIST pidlOut = reinterpret_cast<LPITEMIDLIST>(
m_pMalloc->Alloc(uSize + sizeof(ITEMIDLIST)));
if(pidlOut)
{
LPITEMIDLIST pidlTemp = pidlOut;
// 準備用實際資料充填這個PIDL
pidlTemp->mkid.cb = uSize;
LPPIDLDATA pData = reinterpret_cast<LPPIDLDATA>(pidlTemp->mkid.abID);
// 充填 PIDL
pData->hwnd = hwnd;
//
// 添加更多的字段如果需要...
//
// 一個有0尺寸的PIDL是這個鍊的終止塊
pidlTemp = GetNextItem(pidlTemp);
pidlTemp->mkid.cb = 0;
pidlTemp->mkid.abID[0] = 0;
}
return pidlOut;
}
PIDL必須是平面順序位元組,也就是說,你不能使用指針。如果試圖這樣做,指針将被看作為32位數字,并且引用的位址将丢失。
從PIDL中抽取資訊
所有Shell API函數都使用PIDLs,而且每一個項或檔案夾都以PIDLs的形式來引用。然而,總是能從PIDL中抽取你實際需要處理的資訊。在這種情況下,你需要知道HWND,和是否這個視窗有子視窗。
BOOL CPidlMgr::HasChildren(HWND hWnd)
{
// 确定視窗是否有子視窗
HWND h = GetWindow(hWnd, GW_CHILD);
return (h != NULL);
}
HWND CPidlMgr::GetData(LPCITEMIDLIST pidl)
{
if(!pidl)
return NULL;
// 取得PIDL的最後項,以確定在多重嵌套下獲得正确的HWND
LPITEMIDLIST p = GetLastItem(pidl);
LPPIDLDATA pData = reinterpret_cast<LPPIDLDATA>(p->mkid.abID);
return pData->hwnd;
}
我們知道,PIDL是一個指向由兩個成員組成的結構的指針,頭一個成員表示後面成員的尺寸,是以,探測器知道正确周遊這個連結清單時它需要跳過的位元組數。同樣,命名空間擴充也可以使用這個位元組數,和适當地解釋這個結構。為了簡單起見,PIDL管理器應該定義一些函數來支援PIDL的‘周遊’操作:
LPITEMIDLIST CPidlMgr::GetNextItem(LPCITEMIDLIST pidl)
{
if(pidl)
return reinterpret_cast<LPITEMIDLIST>((reinterpret_cast<LPBYTE>(
const_cast<LPITEMIDLIST>(pidl))) + pidl->mkid.cb);
else
return NULL;
}
LPITEMIDLIST CPidlMgr::GetLastItem(LPCITEMIDLIST pidl)
{
LPITEMIDLIST pidlLast = NULL;
// 取得連結清單中最後一項PIDL
if(pidl)
{
while(pidl->mkid.cb)
{
pidlLast = const_cast<LPITEMIDLIST>(pidl);
pidl = GetNextItem(pidl);
}
}
return pidlLast;
}
PIDL管理器類的另一個任務是提供對象的顯示名。PIDL隻是一個二進制位元組序列,是以,在Shell需要的時候,它将請求檔案夾管理器(實作IShellFolder的對象)提供每一個項的顯示名。因而,在某個地方(如果不是在PIDL管理器類中)應該有一段代碼能夠接受PIDL,并傳回一個顯示名串。一般來講,這個函數應該周遊引用的PIDLs連結清單,建立一個增長的串。如果你浏覽進入檔案夾和子檔案夾,則最内部的項帶有的PIDL包含所有父檔案夾。此後,你應該提供整個串。(在檔案和目錄情況下,路徑名作為顯示名)。其想法是要你來确定對于單個PIDL應該顯示什麼,以及在周遊這個聯系各種塊的鍊之後是用逗号,分号,斜線還是其它什麼來分隔它們。
DWORD CPidlMgr::GetPidlPath(LPCITEMIDLIST pidl, LPTSTR lpszOut)
{
HWND hwnd = GetData(pidl);
TCHAR szClass[100], szTitle[100];
GetWindowText(hwnd, szTitle, 100);
GetClassName(hwnd, szClass, 100);
// 加一個描述到桌面視窗(類 "#32769")
if(!lstrcmpi(szClass, __TEXT("#32769")))
lstrcpy(szClass, __TEXT("Desktop"));
// 以 "标題 [類]"形式傳回串
if(lstrlen(szTitle))
wsprintf(lpszOut, __TEXT("%s [%s]"), szTitle, szClass);
else
wsprintf(lpszOut, __TEXT("[%s]"), szClass);
// 傳回串尺寸
return lstrlen(lpszOut);
}
正如上面代碼所示,在這種情況下操作是容易的,因為傳回的是目前視窗的名,我們不需要考慮到達它所穿過的視窗清單,隻需找到HWND,并傳回适當的資訊即可。然而傳遞給我們的是一個PIDL,是以首先要做的是用GetData()轉換成視窗,然後傳回包含這個視窗标題和類名的格式化串。這個串在每次視窗被選中時都顯示在位址欄和狀态條中。GetPidlPath()方法由檔案夾管理器在GetDisplayNameOf()方法中調用。
在我們的命名空間擴充中所有類都需要處理PIDLs,是以,每一個都将包含LPITEMIDLIST類型的資料成員和PIDL管理器執行個體。
Windows枚舉器
現在讓我們調查一下枚舉器的主要特征。這是一個從IEnumIDList導出的類,它提供對在這個類建立以後系統所有打開的視窗的通路操作。為了運作,它内部定義了PIDLs連結清單并使用它來存儲所有檔案夾項。在類構造器被調用時,所有在作為變量傳遞過來的檔案夾内的項都被枚舉并添加到這個連結清單中。而後這個接口的方法Next()在這個連結清單上工作。無論實作細節如何,這都是實作IEnumIDList接口的類的一般行為。
CEnumIDList::CEnumIDList(HWND hwnd, DWORD dwFlags, HRESULT* pResult)
{
if(pResult)
*pResult = S_OK;
m_pFirst = NULL;
m_pLast = NULL;
m_pCurrent = NULL;
// 建立 PIDL 管理器
m_pPidlMgr = new CPidlMgr();
if(!m_pPidlMgr)
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
// 取得Shell記憶體管理器
if(FAILED(SHGetMalloc(&m_pMalloc)))
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
// 建立項目連結清單
if(!CreateEnumList(hwnd, dwFlags))
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
m_ObjRefCount = 1;
g_DllRefCount++;
}
上面清單是這個類的構造器代碼,它接受一個要枚舉的視窗Handle和要考慮的标志。這個标志是由Shell在調用IShellFolder::EnumObjects()時指定的,我們在這一章的前面已經解釋過了。
在CreateEnumList()方法中,連結清單通過使用EnumChildWindows() SDK函數枚舉視窗而建立。如果基視窗為NULL,說明要枚舉最頂層視窗,這是桌面視窗。這一層上沒有其它視窗,是以不需要枚舉,隻需簡單地通過GetDesktopWindow()獲得桌面視窗的Handle即可,并添加新項到連結清單中。在其它場合我們需要枚舉子視窗和在必要時建立新項:
typedef struct tagENUMWND {LPARAM lParam;
HWND hwndParent;
DWORD dwFlags;
} ENUMWND, FAR* LPENUMWND;
BOOL CEnumIDList::CreateEnumList(HWND hWndRoot, DWORD dwFlags)
{
// 取得桌面視窗
if(hWndRoot == NULL)
{
//如果我們必須考慮根視窗的話(桌面),我們不需要枚舉任何東西,隻要獲得桌面的HWND,
//并添加新元素到連結清單中即可,這就是NewEnumItem()要做的。
hWndRoot = GetDesktopWindow();
NewEnumItem(hWndRoot);
return TRUE;
}
// 枚舉指定視窗的子視窗
ENUMWND ew;
ew.lParam = reinterpret_cast<LPARAM>(this);
ew.hwndParent = hWndRoot;
ew.dwFlags = dwFlags;
//我們需要僅考慮制定視窗的直接子視窗函數,對子視窗的子視窗不感興趣。在
//回調代碼中我們将修改EnumChildWindows的行為。
numChildWindows(hWndRoot, AddToEnumList, reinterpret_cast<LPARAM>(&ew));
return TRUE;
}
BOOL CALLBACK CEnumIDList::AddToEnumList(HWND hwndChild, LPARAM lParam)
{
LPENUMWND lpew = reinterpret_cast<LPENUMWND>(lParam);
//避開不是指定視窗的子視窗的視窗,這就是要跳過那些不是指定視窗的直接子視窗
//的視窗。這個檢查是由EnumChildWindows()的枚舉子視窗和孫視窗屬性給出的,
//我們避開孫視窗。
HWND h = GetParent(hwndChild);
if((h != NULL) && (h != lpew->hwndParent))
return TRUE;
// 儲存lParam 變量中的指針
CEnumIDList* pEnumIDList = reinterpret_cast<CEnumIDList*>(lpew->lParam);
// 重點: 在這裡我們确定什麼是一個檔案夾和什麼是它的葉子,對于視窗,看它是否有子視窗
//探測器希望非檔案夾項
if(lpew->dwFlags & SHCONTF_NONFOLDERS)
return pEnumIDList->NewEnumItem(hwndChild);
//探測器希望檔案夾項
if(lpew->dwFlags & SHCONTF_FOLDERS)
{
// 如果沒有子視窗,扔掉它,因為它已經被添加了。
if(!pEnumIDList->m_pPidlMgr->HasChildren(hwndChild))
return TRUE;
else
pEnumIDList->NewEnumItem(hwndChild);
}
return TRUE;
}
BOOL CEnumIDList::NewEnumItem(HWND hwndChild)
{
LPENUMLIST pNew = NULL;
pNew = reinterpret_cast<LPENUMLIST>(m_pMalloc->Alloc(sizeof(ENUMLIST)));
if(pNew)
{
// 為新元素建立新的PIDL
pNew->pNext = NULL;
pNew->pidl = m_pPidlMgr->Create(hwndChild);
// 者是否為清單中的第一項
if(!m_pFirst)
{
m_pFirst = pNew;
m_pCurrent = m_pFirst;
}
// 添加新項到清單尾
if(m_pLast)
m_pLast->pNext = pNew;
// 更新最後項指針
m_pLast = pNew;
return TRUE;
}
return FALSE;
}
就我們的目的而言,EnumChildWindows()函數有一個缺陷是我們應該克服的,它傳回指定視窗的所有子視窗,甚至是子視窗的子視窗。而我們希望的是直接子視窗。然而EnumChildWindows()函數可以傳遞一個回調函數對每一個枚舉的視窗進行操作。在我們的例子中這個函數是AddToEnumList(),它檢查枚舉視窗實際的父視窗,如果父視窗與期望的不比對,則拒絕它。
對每一個視窗我們都需要建立PIDL,并把它加到清單中,在上面的清單中這是由NewEnumItem()輔助函數來完成的。還要注意回調函數仔細地根據Shell需求枚舉視窗:僅僅是檔案夾,還是檔案夾和項。
取得下一項
一旦建立了清單視窗,傳回其中的 n 個項到調用者就是周遊這個清單并充填一個數組。注意,我們要求建立和傳回一個PIDL的新拷貝,Shell将使用和釋放它們。
STDMETHODIMP CEnumIDList::Next(DWORD dwElements, LPITEMIDLIST apidl[],
LPDWORD pdwFetched)
{
DWORD dwIndex;
HRESULT hr = S_OK;
if(dwElements > 1 && !pdwFetched)
return E_INVALIDARG;
for(dwIndex = 0 ; dwIndex < dwElements ; dwIndex++)
{
// 是否為清單中的最後一個元素
if(!m_pCurrent)
{
hr = S_FALSE;
break;
}
// 拷貝 PIDLs
apidl[dwIndex] = m_pPidlMgr->Copy(m_pCurrent->pidl);
m_pCurrent = m_pCurrent->pNext;
}
// 傳回取得項目數
if(pdwFetched)
*pdwFetched = dwIndex;
return hr;
}
檔案夾管理器
這個與Shell聯系最緊密的類是實作IShellFolder接口的類。它必須建立枚舉器和觀察,以及通過建立新的CShellFolder類執行個體綁定子檔案夾。它還必須提供每一個PIDL的顯示名,和指向附加接口IContextMenu 和 IExtractIcon 的指針。
BindToObject()是用于綁定子檔案夾的方法。它簡單地建立一個由IShellFolder接口導出的類,并傳遞接收的PIDL作為構造器的變量。其結果是一個新的Shell子檔案夾被建立。
STDMETHODIMP CShellFolder::BindToObject(LPCITEMIDLIST pidl, LPBC pbcReserved,
REFIID riid, LPVOID* ppvOut)
{
CShellFolder* pShellFolder = new CShellFolder(this, pidl);
if(!pShellFolder)
return E_OUTOFMEMORY;
HRESULT hr = pShellFolder->QueryInterface(riid, ppvOut);
pShellFolder->Release();
return hr;
}
下面代碼段顯示檔案夾對象是怎樣建立它的觀察和枚舉器對象的。對于枚舉器首先要抽取要枚舉視窗的HWND。然後才能建立枚舉器對象:
STDMETHODIMP CShellFolder::CreateViewObject(HWND hwndOwner, REFIID riid,
LPVOID* ppvOut)
{
CShellView* pShellView = new CShellView(this, m_pidl);
if(!pShellView)
return E_OUTOFMEMORY;
m_pShellView = pShellView;
HRESULT hr = pShellView->QueryInterface(riid, ppvOut);
pShellView->Release();
return hr;
}
STDMETHODIMP CShellFolder::EnumObjects(HWND hwndOwner, DWORD dwFlags,
LPENUMIDLIST* ppEnumIDList)
{
// hwndOwner 隻是用于任何消息框的父視窗的 HWND
HRESULT hr;
*ppEnumIDList = NULL;
HWND hWnd = m_pPidlMgr->GetData(m_pidl);
*ppEnumIDList = new CEnumIDList(hWnd, dwFlags, &hr);
if(*ppEnumIDList == NULL)
return hr;
return S_OK;
}
對于顯示名,檔案夾管理器最終調用PIDL管理器類,但是在傳回之前必須轉換串到Unicode字元。GetDisplayNameOf()實際傳回STRRET,它是一個展示三種可能類型串的一種聯合類型資料結構:Unicode串,ANSI串,串的偏移(見第5章)。此時我們選擇使用Unicode串:
STDMETHODIMP CShellFolder::GetDisplayNameOf(LPCITEMIDLIST pidl,
DWORD dwFlags, LPSTRRET lpName)
{
TCHAR szText[MAX_PATH] = {0};
// 取得作窗框,位址欄等的顯示名
m_pPidlMgr->GetPidlPath(pidl, szText);
// 必須轉換串到Unicode,是以配置設定寬字元串
int cchOleStr = lstrlen(szText) + 1;
lpName->pOleStr = reinterpret_cast<LPWSTR>(m_pMalloc->Alloc(cchOleStr *
sizeof(WCHAR)));
if(!lpName->pOleStr)
return E_OUTOFMEMORY;
lpName->uType = STRRET_WSTR;
mbstowcs(lpName->pOleStr, szText, cchOleStr);
return S_OK;
}
然而,檔案夾管理器活動的最值得關注的部分是比較項和傳回項屬性操作。
比較項
比較項的功能實作起來相對比較簡單一點,但是它有極其重要的作用。如果不能保證它絕對正确地實作,最好的辦法就是不支援它。在開發命名空間擴充的前期階段,我們部分地實作IShellFolder::CompareIDs()方法,你必須想到有多少錯誤行為是我們必須克服的。更壞的情況是幾乎隻有很少的工作需要項的比較例程來做。僅在決定适當地安排它工作時——差不多是夢幻般地——突然開始工作。
CompareIDs()成員函數從Shell接收兩個PIDLs參數,并傳回表示那一個項較大的值。(非0)的正值表示第一個大于第二個,而負值表示相反,空值說明項是相等的。
從線上資料中你可能已經證明,CompareIDs()還有第三個參數,lParam,這表示使用的分類規則。習慣上,0值是指你應該通過檔案夾内容的名字排序,非0值表示檔案夾專有規則。在這個應用中,當使用者點選清單觀察頂部的列标題控件時,CompareIDs()方法獲得調用。(這是探測器使用者界面的典型行為)。我們在Shell的觀察代碼中處理這個事件。是以完全由我們來決定通過lParam傳遞什麼值。
在這個實作中,我們決定忽略lParam的選擇,代之使用CShellFolder類的資料成員m_uSortField來表示怎樣排序内容。理由是我們需要保持目前排序字段的蹤迹。是以這個資料成員無論如何是需要的。更進一步,我們希望能升序或降序排列——如果一個列已經被分類,則再次點選将反序。下面清單顯示我們是怎樣做的:
STDMETHODIMP CShellFolder::CompareIDs(LPARAM lParam, LPCITEMIDLIST pidl1,
LPCITEMIDLIST pidl2)
{
//這個函數總是用0設定lParam來調用。習慣上這是用名字分類,非0值表示特殊的檔案夾分類規則
//注意,這裡我們使用m_uSortField資料成員作為對lParam的替換。
HWND hwnd1 = m_pPidlMgr->GetData(pidl1);
HWND hwnd2 = m_pPidlMgr->GetData(pidl2);
// 由子檔案夾分類 CHILDREN
if(m_uSortField == 1 || m_uSortField == -1)
{
int fChildren1 = m_pPidlMgr->HasChildren(hwnd1);
int fChildren2 = m_pPidlMgr->HasChildren(hwnd2);
if(fChildren1 < fChildren2)
return m_uSortField;
else if(fChildren1 > fChildren2)
return m_uSortField * -1;
else
return 0;
}
// 按類分類 CLASS
if(m_uSortField == 2 || m_uSortField == -2)
{
// BUFSIZE 是符号内容尺寸,設定為100
TCHAR szClass1[BUFSIZE];
TCHAR szClass2[BUFSIZE];
GetClassName(hwnd1, szClass1, BUFSIZE);
GetClassName(hwnd2, szClass2, BUFSIZE);
return m_uSortField * lstrcmpi(szClass1, szClass2);
}
// 按标題分類 TITLE
if(m_uSortField == 4 || m_uSortField == -4)
{
TCHAR szTitle1[BUFSIZE];
TCHAR szTitle2[BUFSIZE];
GetWindowText(hwnd1, szTitle1, BUFSIZE);
GetWindowText(hwnd2, szTitle2, BUFSIZE);
return m_uSortField * lstrcmpi(szTitle1, szTitle2);
}
// 按Handle分類 HWND
if(hwnd1 < hwnd2)
return m_uSortField;
else if(hwnd1 > hwnd2)
return m_uSortField * -1;
else
return 0;
}
這個函數使我們可以用四種方法對觀察中的項目進行分類:用子視窗,用類名,用Handle和用标題。更進一步,通過使m_uSortField值為正或負,以及乘以比較結果,可以轉換分類順序。依據問題中的比較邏輯,我們或者比較HWND,或者借助lstrcmpi()來比較串。
檔案夾屬性
要使命名空間擴充在探測器内正常工作,傳回正确的檔案夾屬性是至關重要的。例如,在打開主節點時,你希望看到它包含的所有檔案夾。在視窗的概念下,當你展開相對于桌面的視窗節點時,你應該看到所有頂層視窗和所有沒有父視窗的視窗,此外沒有其它的東西。而後,在這些二級視窗上點選時,希望看到視窗的所有子視窗。
GetAttributesOf()方法期望對一組由PIDLs指定的項傳回正确的特征。為了指派特征,使用SFGAO_XXX常量,這是我們在第4章所看到的。記住,可以同時有多個PIDLs傳遞給你,要求它們的多個特征。
STDMETHODIMP CShellFolder::GetAttributesOf(UINT uCount, LPCITEMIDLIST aPidls[],
LPDWORD pdwAttribs)
{
*pdwAttribs = -1;
for(UINT i = 0 ; i < uCount ; i++)
{
DWORD dwAttribs = 0;
// 這個項是一個視窗嗎?
HWND hwnd = m_pPidlMgr->GetData(aPidls[i]);
if(IsWindow(hwnd))
{
//這裡以普通的方式配置設定你想要這個項所具有的風格,當然最終是你來适當地管理它們。
if(m_pPidlMgr->HasChildren(hwnd))
{
dwAttribs |= SFGAO_FOLDER;
if(m_pPidlMgr->HasChildrenOfChildren(hwnd))
dwAttribs |= SFGAO_HASSUBFOLDER;
}
}
*pdwAttribs = dwAttribs;
}
return S_OK;
}
GetAttributesOf()函數接收PIDLs清單以恢複其特征。我們說每一個有子視窗的視窗是檔案夾,是以很容易确定什麼時候需要SFGAO_FOLDER屬性。如果我們在這一點上停止,Shell就會漏掉使這個節點可展開的特征,這使它不可能浏覽超過一層。是以,我們需要表示不僅能指定節點是否有子節點,還要給出是否這些子節點有它自己的子節點。這個問題要回答是,可以使用SFGAO_HASSUBFOLDER标志。
回到PIDL管理類,HasChildrenOfChildren()函數簡單地枚舉子視窗并檢查其中是否有子視窗:
BOOL CPidlMgr::HasChildrenOfChildren(HWND hWnd)
{
// 确定是否視窗有子視窗
BOOL b = FALSE;
EnumChildWindows(hWnd, WindowHasChildren, reinterpret_cast<LPARAM>(&b));
return b;
}
BOOL CPidlMgr::WindowHasChildren(HWND hwnd, LPARAM lParam)
{
BOOL* pB = reinterpret_cast<BOOL*>(lParam);
// 如果這個視窗是一個子視窗,傳回的HWND不是NULL
HWND h = GetWindow(hwnd, GW_CHILD);
*pB = (h != NULL); // TRUE 如果至少有一個孫視窗存在
return(h == NULL); // 需要傳回FALSE 來終止枚舉
}
這段代碼有幾個重點,要檢查給定視窗是否有孫視窗,必須枚舉所有子視窗。EnumChildWindows()是做這個工作的API函數。與任何其它枚舉一樣,從第一個視窗開始,并終止于最後一個視窗。每一個被找到的視窗為進一步處理都被傳遞給指定的回調函數,在此時是WindowHasChildren()函數。
這是因為我們需要知道是否至少有一個孫視窗,是以隻要發現一個,我們就通過在回調函數中傳回FALSE停止枚舉。反過來我們還需要通知調用函數HasChildrenOfChildren(),枚舉由于找到孫視窗而完成。傳遞給EnumChildWindows()的布爾值就用于這個結果的緩沖:
EnumChildWindows(hWnd, WindowHasChildren, reinterpret_cast<LPARAM>(&b));
較簡單的方法是每次設定SFGAO_FOLDER時都設定SFGAO_HASSUBFOLDER标志,這可以有效地工作,但有一個小bug:所有節點都将顯示可展開,無論它是否有子檔案夾。通過适當的操作,我們得到如圖所示的樹觀察:
視窗觀察
本質上,觀察是一個視窗。正常情況,你建立兩個視窗:一個父視窗包含一個子視窗。這個子視窗就是使用者看到的和與之互動的視窗。我們在這裡建立的觀察将是一個客戶類視窗以避免子類化。另一項可以使用的技術要求用一些要素控件定義非模式對話框。
在這種情況下,子視窗是一個清單觀察,之是以這樣選擇是想要使它看上去象傳統的檔案夾觀察。它小心的響應某些來自系統的消息,包括WM_CREATE,WM_SIZE,WM_SETFOCUS,和 WM_NOTIFY這些重要消息。注意,在你建立實作這個觀察的類時,WndProc()(觀察視窗的視窗過程)必須是靜态成員。當然,靜态成員不能獲得this指針,因而,它不能通路其它類成員。是以我們需要找到其它辦法來把this指針傳遞給WndProc()代碼。我們的方案是在IShellView::CreateViewWindow()中建立觀察視窗時,傳遞this作為CreateWindowEx()的最後一個變量:
hWnd = CreateWindowEx(0, NS_CLASS_NAME, NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
prcView->left, prcView->top,
prcView->right - prcView->left,
prcView->bottom - prcView->top,
m_hwndParent, NULL, g_hInst, this);
這使得this指針通過WM_NCCREATE消息傳遞給視窗過程。然而在類的靜态成員中通路this還不夠:我們還需要使它永久地保持,以便完成我們需要視窗過程産生的各個調用。解決這個問題的典型方案是把這個指針儲存到視窗的額外子節——與每一個視窗關聯的,完全由程式員支配的32位緩沖,下面代碼說明了應該怎樣設定它:
SetWindowLong(hWnd, GWL_USERDATA, reinterpret_cast<LONG>(pThis));
下面一行說明怎樣讀出它:
CShellView* pThis = reinterpret_cast<CShellView*>(GetWindowLong(hWnd,
GWL_USERDATA));
有了pThis指針,我們就可以從不是類成員的過程内部調用類的所有公共成員。(靜态類成員從文法角度看屬于類,但是它實際是一個依附于類的全程函數)。下面是觀察視窗過程的代碼:
LRESULT CALLBACK CShellView::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam)
{
CShellView* pThis = reinterpret_cast<CShellView*>(GetWindowLong(hWnd,
GWL_USERDATA));
switch(uMsg)
{
case WM_NCCREATE:
{
LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
pThis = reinterpret_cast<CShellView*>(lpcs->lpCreateParams);
SetWindowLong(hWnd, GWL_USERDATA, reinterpret_cast<LONG>(pThis));
pThis->m_hWnd = hWnd;
}
break;
case WM_CONTEXTMENU:
return pThis->OnContextMenu();
case WM_MENUSELECT:
return pThis->OnMenuSelect(LOWORD(wParam));
case WM_SIZE:
return pThis->OnSize(LOWORD(lParam), HIWORD(lParam));
case WM_CREATE:
return pThis->OnCreate();
case WM_SETFOCUS:
return pThis->OnSetFocus();
case WM_KILLFOCUS:
return pThis->OnKillFocus();
case WM_ACTIVATE:
return pThis->OnActivate(SVUIA_ACTIVATE_FOCUS);
case WM_COMMAND:
return pThis->OnCommand(GET_WM_COMMAND_ID(wParam, lParam),
GET_WM_COMMAND_CMD(wParam, lParam),
GET_WM_COMMAND_HWND(wParam, lParam));
case WM_NOTIFY:
return pThis->OnNotify(wParam, reinterpret_cast<LPNMHDR>(lParam));
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
為了保證選中項與菜單和工具條狀态之間有完美的結合,處理WM_SETFOCUS 和 WM_KILLFOCUS是特别重要的:
LRESULT CShellView::OnSetFocus()
{
// 告訴浏覽器我們有焦點
m_pShellBrowser->OnViewWindowActive(this);
OnActivate(SVUIA_ACTIVATE_FOCUS);
return 0;
}
// OnKillFocus
LRESULT CShellView::OnKillFocus()
{
OnActivate(SVUIA_ACTIVATE_NOFOCUS);
return 0;
}
我們在WM_CREATE消息期間建立這個清單觀察,并在WM_SIZE期間調整它的尺寸以便使觀察覆寫整個可用的區域。
清單觀察風格
在響應WM_CREATE時清單觀察的建立要設定下列風格:
WS_TABSTOP WS_VISIBLE WS_CHILD WS_BORDER
LVS_SINGLESEL LVS_REPORT LVS_SHAREIMAGELISTS
此外,由于要求Windows95作為最小平台,還需要加入一些新的清單觀察風格。這些必須通過新的宏進行設定的風格稱為擴充風格。
BOOL CShellView::CreateList()
{
DWORD dwStyle = WS_TABSTOP | WS_VISIBLE | WS_CHILD | WS_BORDER |
LVS_SINGLESEL | LVS_REPORT | LVS_SHAREIMAGELISTS;
// 建立清單觀察
m_hwndList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTVIEW, NULL, dwStyle,
0, 0, 0, 0, m_hWnd,reinterpret_cast<HMENU>(ID_LISTVIEW),g_hInst, NULL);
if(!m_hwndList)
return FALSE;
// 設定擴充風格
DWORD dwExStyle = LVS_EX_TRACKSELECT | LVS_EX_UNDERLINEHOT |
LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP;
ListView_SetExtendedListViewStyle(m_hwndList, dwExStyle);
return TRUE;
}
由這些标志設定的風格說明現在使用滑鼠選擇是整行選擇(不是隻選擇頭一項),可以通過拖拽移動列。下面是在清單觀察建立後運作的源代碼。它定義了四個列,并使用項填寫這個清單:
BOOL CShellView::InitList()
{
TCHAR szString[MAX_PATH] = {0};
// 清空清單觀察
ListView_DeleteAllItems(m_hwndList);
// 初始化列
LV_COLUMN lvColumn;
lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
lvColumn.fmt = LVCFMT_LEFT;
lvColumn.pszText = szString;
lvColumn.cx = g_nColumn1;
LoadString(g_hInst, IDS_COLUMN1, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 0, &lvColumn);
RECT rc;
GetClientRect(m_hWnd, &rc);
lvColumn.cx = g_nColumn2;
LoadString(g_hInst, IDS_COLUMN2, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 1, &lvColumn);
lvColumn.cx = g_nColumn3;
LoadString(g_hInst, IDS_COLUMN3, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 2, &lvColumn);
lvColumn.cx = g_nColumn4;
LoadString(g_hInst, IDS_COLUMN4, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 3, &lvColumn);
ListView_SetImageList(m_hwndList, g_himlSmall, LVSIL_SMALL);
return TRUE;
}
這個清單觀察然後用從檔案夾的枚舉器中使用其Next()方法獲得的資訊進行填充。枚舉器通過觀察的m_pSFParent元素,一個指向IShellFolder的指針來建構,這在其構造時就被取得并存儲了。
void CShellView::FillList()
{
LPENUMIDLIST pEnumIDList = NULL;
//獲得檔案夾的枚舉器對象。通過CShellView構造器接收的指針調用EnumObjects()
HRESULT hr = m_pSFParent->EnumObjects(m_hWnd,
SHCONTF_NONFOLDERS | SHCONTF_FOLDERS, &pEnumIDList);
if(SUCCEEDED(hr))
{
LPITEMIDLIST pidl = NULL;
// 停止重繪以避免抖動
SendMessage(m_hwndList, WM_SETREDRAW, FALSE, 0);
// 添加項
DWORD dwFetched;
while((pEnumIDList->Next(1, &pidl, &dwFetched) == S_OK) && dwFetched)
{
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iItem = ListView_GetItemCount(m_hwndList);
// 存儲PIDL 到 HWND, 使用項的lParam成員
HWND h = m_pPidlMgr->GetData(pidl);
lvi.lParam = reinterpret_cast<LPARAM>(m_pPidlMgr->Create(h));
// 列 1: 狀态 (也設定圖像)
TCHAR szState[30] = {0};
if(m_pPidlMgr->HasChildren(h))
{
lvi.iImage = 0;
LoadString(g_hInst, IDS_CHILDREN, szState, 30);
}else{
lvi.iImage = 1;
LoadString(g_hInst, IDS_NOCHILDREN, szState, 30);
}
lvi.pszText = szState;
// 添加項
int i = ListView_InsertItem(m_hwndList, &lvi);
// 填寫子項 2: HWND
TCHAR szBuf[MAX_PATH] = {0};
wsprintf(szBuf, __TEXT("0x%04X"), h);
ListView_SetItemText(m_hwndList, i, 2, szBuf);
// 填寫子項 3: 标題
GetWindowText(h, szBuf, MAX_PATH);
ListView_SetItemText(m_hwndList, i, 3, szBuf);
// 填寫子項 1: 類
GetClassName(h, szBuf, MAX_PATH);
ListView_SetItemText(m_hwndList, i, 1, szBuf);
}
// 初始用 HWND 分類項
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast<LPARAM>(m_pSFParent));
// 重繪清單觀察
SendMessage(m_hwndList, WM_SETREDRAW, TRUE, 0);
InvalidateRect(m_hwndList, NULL, TRUE);
UpdateWindow(m_hwndList);
pEnumIDList->Release();
}
}
注意,在清單觀察中我們儲存了項的PIDL的拷貝,并把它存儲在項的lparam成員中,如果覺得奇怪為什麼需要這樣,因為我們後面将用到它。
列分類
當滑鼠在清單觀察的标題上點選時,我們希望引起觀察中的項用IShellFolder::CompareIDs()方法在背景分類。在任何列上分類都是可能的,而實際安排這樣做的則是我們的另一個任務。首先,我們需要感覺使用者在列上的點選,這是容易的:通過WM_NOTIFY消息這個事件通知到觀察。我們可以在主視窗過程中解釋這個事件,并使用下面代碼處理它.
LRESULT CShellView::OnNotify(UINT CtlID, LPNMHDR lpnmh)
{
switch(lpnmh->code)
{
case NM_SETFOCUS:
OnSetFocus();
break;
case NM_KILLFOCUS:
OnDeactivate();
break;
case HDN_ENDTRACK:
g_nColumn1 = ListView_GetColumnWidth(m_hwndList, 0);
g_nColumn2 = ListView_GetColumnWidth(m_hwndList, 1);
g_nColumn3 = ListView_GetColumnWidth(m_hwndList, 2);
g_nColumn4 = ListView_GetColumnWidth(m_hwndList, 3);
return 0;
case HDN_ITEMCLICK:
{
NMHEADER* pNMH = reinterpret_cast<NMHEADER*>(lpnmh);
// 需要保留順序碼?
if(m_pSFParent->m_uSortField == 1 + pNMH->iItem)
m_pSFParent->m_uSortField = (-1) * (1 + pNMH->iItem);
else
m_pSFParent->m_uSortField = 1 + pNMH->iItem;
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast<LPARAM>(m_pSFParent));
}
return 0;
case LVN_ITEMACTIVATE:
{
LV_ITEM lvItem;
ZeroMemory(&lvItem, sizeof(LV_ITEM));
lvItem.mask = LVIF_PARAM;
LPNMLISTVIEW lpnmlv = reinterpret_cast<LPNMLISTVIEW>(lpnmh);
lvItem.iItem = lpnmlv->iItem;
ListView_GetItem(m_hwndList, &lvItem);
m_pShellBrowser->BrowseObject(
reinterpret_cast<LPITEMIDLIST>(lvItem.lParam),
SBSP_DEFBROWSER | SBSP_RELATIVE);
return 0;
}
}
return 0;
}
WM_NOTIFY消息主要用于處理列的尺寸變化(HDN_ENDTRACK),列标題點選(HDN_ITEMCLICK),和輕按兩下(或單擊,如果設定了檔案夾的Web風格選項)單個項(LVN_ITEMACTIVATE)。要分類特定字段,我們隻需點選标題即可。系統傳遞來的是從0開始的列索引。如果這個值(pNMH->iItem)與m_uSortField值一緻(即,一個從一開始的列索引,是目前分類列),則保留排序,在我們的代碼中簡單地是用-1乘以 m_uSortField值。與IShellFolder::CompareIDs()方法組合将導緻我們期望的結果。
if(m_pSFParent->m_uSortField == 1 + pNMH->iItem)
m_pSFParent->m_uSortField = (-1) * (1 + pNMH->iItem);
else
m_pSFParent->m_uSortField = 1 + pNMH->iItem;
要實際分類清單觀察,必須調用
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast<LPARAM>(m_pSFParent));
這裡CompareItems()是使用者定義的全程函數,它最終調用IShellFolder::CompareIDs():
int CALLBACK CompareItems(LPARAM lParam1, LPARAM lParam2, LPARAM lpData)
{
CShellFolder* pFolder = reinterpret_cast<CShellFolder*>(lpData);
if(!pFolder)
return 0;
return pFolder->CompareIDs(0, reinterpret_cast<LPITEMIDLIST>(lParam1),
reinterpret_cast<LPITEMIDLIST>(lParam2));
}
下圖顯示了項通過HWND分類的觀察,遞減順序:
在類名列上點選,我們可以得到下面結果:
浏覽視窗
在任何給出的檔案夾觀察中,通常都應該有子檔案夾和‘葉’視窗。在處理實際檔案夾時,探測器的使用者界面使你能通過輕按兩下子檔案夾浏覽進它的内部。然而在我們寫的命名空間擴充中這個特征不是自動的,我們必須自己實作它。有兩點需要解決:
取得標明清單觀察項的PIDL
打開新的檔案夾
項的PIDL是容易取得的,因為我們特意把它與項存儲到一起了。下面是怎樣取得它的代碼,這是在CShellView::OnNotify()函數的LVN_ITEMACTIVATE處理段的代碼:
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_PARAM;
LPNMLISTVIEW lpnmlv = reinterpret_cast<LPNMLISTVIEW>(lpnmh);
lvi.iItem = lpnmlv->iItem;
ListView_GetItem(m_hwndList, &lvi);
m_pShellBrowser->BrowseObject(reinterpret_cast<LPITEMIDLIST>(lvi.lParam),
SBSP_DEFBROWSER | SBSP_RELATIVE);
首先,我們準備一個LV_ITEM結構,以便由ListView_GetItem()對其進行填寫。在屏蔽成員上,我們設定标志表示需要的資訊類型——lParam值與之相關。此後調用IShellBrowser::BrowseObject(),傳遞要浏覽的PIDL。特别要注意的是這個方法的第二個參數:
HRESULT IShellBrowser::BrowseObject(LPCITEMIDLIST pidl, UINT wFlags);
這個UNIT數用于驅動BrowseObject()行為,我們指定的第一個标志是SBSP_DEFBROWSER,意思是希望用目前觀察的相同選項打開新檔案夾:不是新視窗,不是不同觀察設定。對于調用這個方法的函數這一定是最一般的選擇。第二個标志SBSP_RELATIVE說明PIDL是相對于目前檔案夾的。在這種情況下,這是明智的選擇,因為我們在項的lparam成員中存儲的是相對的PIDL——它僅僅涉及特定的視窗,忽略任何關于其父視窗的資訊。是以,如果使用SBSP_ABSOLUTE,将産生新的空檔案夾。
給出使用者界面
在我們的命名空間擴充得到焦點後,它可以改變探測器的菜單和工具條。這種改變應該在擴充接收聚焦時産生,并且在失去焦點時消除。
IShellView::UIActivate()方法獲得Shell調用來通知你的擴充它是活動的還是不活動的,是以可以在這個函數中産生對菜單的改變和對使用者界面其它設定的修改。在下面的代碼中我們也注意到了狀态條,而且這也是改變工具條的正确地點,在前面我們已經讨論了這一點。
STDMETHODIMP CShellView::UIActivate(UINT uState)
{
// 如果遇上一次比較狀态沒有改變,則退出
if(m_uState == uState)
return S_OK;
// 修改菜單
OnActivate(uState);
// 修改狀态條
if(uState != SVUIA_DEACTIVATE)
{
TCHAR szName[MAX_PATH] = {0};
// 如果需要添加更多部分, 它等價于 SB_SIMPLE
int aParts[1] = {-1};
// 設定部分數
m_pShellBrowser->SendControlMsg(FCW_STATUS, SB_SETPARTS, 1,
reinterpret_cast<LPARAM>(aParts), NULL);
m_pPidlMgr->GetPidlPath(m_pidl, szName);
m_pShellBrowser->SendControlMsg(FCW_STATUS, SB_SETTEXT, 0,
reinterpret_cast<LPARAM>(szName), NULL);
}
return S_OK;
}
狀态條是一個帶,通常放在頂層視窗的底部——在探測器場合。它可以劃分成幾部分,每一部分都有它們自己的外觀設定(插入,平面,凸起),要設定狀态條的各個部分,是用SB_SETPARTS消息,它是用整數組來表示各個部分的右邊緣(lparam 變量),另一個整數表示部分數(wparam 變量)。
描述每一節右邊緣的數組值以客戶坐标表示。如果一個值是 -1(如我們的例子),這一部分是要擴充到駐在視窗的右邊緣的。一個隻有單個部分的狀态條實際是由SB_SIMPLE消息定義的更簡單的狀态條,它使所有已存在的部分被删除。使用部分數組則是:
int aParts[1] = {-1};
發送消息SB_SIMPLE将産生與上面代碼同樣的效果。然而,如果我們确實在這裡這樣做的話,你将不會看到關于狀态條的這些細節資訊。
菜單修改
修改菜單要求三步操作,象我們早期描述的,首先需要建立新菜單,然後把它與菜單描述符一起傳遞給探測器。而後探測器以标準方式填寫菜單,此時,你可以選擇修改它。
在我們的例子中,需要添加‘視窗觀察’頂層菜單。進一步,我們不需要‘編輯’菜單(因為對于打開視窗的清單不需要操作),和改變‘幫助|關于…’視窗。
LRESULT CShellView::OnActivate(UINT uState)
{
// 上次調用之後狀态是否改變?
if(m_uState == uState)
return S_OK;
// 銷毀所有以前對菜單的改變
OnDeactivate();
// 如果活動...
if(uState != SVUIA_DEACTIVATE)
{
// 步驟 1: 建立新菜單
m_hMenu = CreateMenu();
if(m_hMenu)
{
// 步驟 2: 通過菜單組描述符與Shell共享它
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(m_hMenu, &omw);
// 步驟 3: 改變菜單
// 步驟 3.1: 建立并插入 \'視窗觀察\' 頂層菜單
TCHAR szText[MAX_PATH] = {0};
LoadString(g_hInst, IDS_MI_WINVIEW, szText, MAX_PATH);
MENUITEMINFO mii;
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_SUBMENU | MIIM_TYPE | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.hSubMenu = BuildWinViewMenu();
if(mii.hSubMenu)
InsertMenuItem(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii);
// 步驟 3.2: 取得幫助菜單,并合并
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU;
if(GetMenuItemInfo(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii))
MergeHelpMenu(mii.hSubMenu);
// 步驟 3.3: 删除‘編輯’菜單
DeleteMenu(m_hMenu, FCIDM_MENU_EDIT, MF_BYCOMMAND);
// 步驟 3.4: 如果有焦點,加項到檔案菜單
if(uState == SVUIA_ACTIVATE_FOCUS)
{
// 獲得‘檔案’菜單 并合并
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU;
if(GetMenuItemInfo(m_hMenu, FCIDM_MENU_FILE, FALSE, &mii))
MergeFileMenu(mii.hSubMenu);
}
// 設定新菜單
m_pShellBrowser->SetMenuSB(m_hMenu, NULL, m_hWnd);
}
}
// 儲存目前狀态
m_uState = uState;
return 0;
}
注意,我們應該怎樣用在shlobj.h中預定義的常量來引用系統菜單。例如,在‘幫助’菜單前加新菜單,我們使用:
InsertMenuItem(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii);
用這個調用,InsertMenuItem()插入新菜單到第二個參數指定的菜單之前。BuildWinViewMenu()是有如下形式的輔助函數:
HMENU CShellView::BuildWinViewMenu()
{
HMENU hSubMenu = CreatePopupMenu();
if(hSubMenu)
{
TCHAR szText[BUFSIZE] = {0};
MENUITEMINFO mii;
// 加 "屬性" 到 "視窗觀察"
LoadString(g_hInst, IDS_MI_PROPERTIES, szText, BUFSIZE);
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_TYPE | MIIM_ID | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.wID = IDM_WIN_PROPERTIES;
// 加到菜單尾部
InsertMenuItem(hSubMenu, static_cast<UINT>(-1), TRUE, &mii);
// 加 "程序觀察" 到 "視窗觀察"
LoadString(g_hInst, IDS_MI_PROCESSVIEW, szText, BUFSIZE);
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_TYPE | MIIM_ID | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.wID = IDM_WIN_PROCESS;
// 加到菜單尾部
InsertMenuItem(hSubMenu, static_cast<UINT>(-1), TRUE, &mii);
}
return hSubMenu;
}
預設情況下,在‘檔案’菜單下的新項(以及任何我們建立的工具條按鈕)僅當擴充有焦點時出現。如果在右窗框中選擇項,它仍然沒有焦點,是以你必須等待合并你的客戶項到‘檔案’菜單。在擴充丢失焦點時,好的方法是指令我們删除所有痕迹:
void CShellView::OnDeactivate()
{
if(m_uState != SVUIA_DEACTIVATE)
{
if(m_hMenu)
{
m_pShellBrowser->SetMenuSB(NULL, NULL, NULL);
m_pShellBrowser->RemoveMenusSB(m_hMenu);
DestroyMenu(m_hMenu);
m_hMenu = NULL;
}
m_uState = SVUIA_DEACTIVATE;
}
}
顯示幫助文字
下圖顯示當視窗觀察擴充活動時探測器的新菜單。
注意出現在狀态條中的幫助文字。為了允許這個特征,你簡單地需要在視窗過程中處理WM_MENUSELECT消息。就象在老Windows程式設計時所做的那樣。有兩種方法将文字設定到狀态條上。使用SendControlMsg()直接發送消息到視窗,或通過調用SetStatusTextSB()自己設定文字。雖然資料中推薦使用後者,IShellBrowser還是輸出了這兩個函數。這是合理的(這個方法封裝了明顯的狀态條消息,并且在未來也是不變化的),但是這也暴露了一個問題,在涉及到具有多個部分的狀态條時,這個函數就是不充分的了:SetStatusTextSB()不允許指定想要設定那一部分。此外,我們早期提到過,SetStatusTextSB()使用Unicode串。
連接配接關聯菜單與項
Shell通過查找IContextMenu接口自動搜尋關聯菜單。對于這個例子我們實作這個接口,并添加兩個項。第一個是拷貝視窗顯示名到剪裁闆,第二個是顯示視窗屬性的對話框:
如圖所示,這個對話框傳達的資訊包括建立視窗的可執行程式名,以及圖示。特别為了得到圖示,我們使用了下面代碼:
//傳回指定視窗的大/小圖示備份,或标準圖示,如果沒有屬于這個視窗類的圖示。
HICON GetWindowIcon(HWND hwnd, BOOL fBig)
{
HICON hIcon = NULL;
//首先搜尋指定到視窗類的圖示。如果沒有找到,在試圖通過WM_GETICON取得指
//派給特定視窗的圖示。如果失敗,使用标準圖示。
if(fBig)
{
// 要求大圖示
hIcon = reinterpret_cast<HICON>(GetClassLong(hwnd, GCL_HICON));
if(hIcon == NULL)
hIcon = reinterpret_cast<HICON>(SendMessage(hwnd, WM_GETICON,
ICON_BIG, 0));
}else{
hIcon = reinterpret_cast<HICON>(GetClassLong(hwnd, GCL_HICONSM));
if(hIcon == NULL)
hIcon = reinterpret_cast<HICON>(SendMessage(hwnd, WM_GETICON,
ICON_SMALL, 0));
}
if(hIcon == NULL)
hIcon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_PARWND));
// 傳回圖示備份
return CopyIcon(hIcon);
}
首先檢查類圖示,如果沒有找到,則改為發送WM_GETICON消息到視窗。如果這個方法也失敗,則使用标準圖示。無論什麼結果,我們都傳回一個圖示的備份,而不是初始圖示。調用者負責釋放這個圖示。
對于找出建立給定視窗的可執行程式名,我們使用一個WDJ文章中給出的竅門。它使用Winodws9x下的‘ToolHelp’API,以及WindowsNT4.0下的PSAPI庫。在第15章也涉及到了這個科目。
聯接關聯菜單與項就象實作IContextMenu接口函數那樣容易:
STDMETHODIMP CContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
WORD wCmd = LOWORD(lpcmi->lpVerb);
switch(wCmd)
{
case 1: // 屬性
ShowProperties();
break;
case 0: // 拷貝
CopyTextToClipboard();
break;
}
return S_OK;
}
STDMETHODIMP CContextMenu::GetCommandString(UINT, UINT, UINT*, LPSTR, UINT)
{
return E_NOTIMPL;
}
STDMETHODIMP CContextMenu::QueryContextMenu(HMENU hmenu,UINT indexMenu,
UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
UINT idCmd = idCmdFirst;
// 添加新項,從資源中裝入串
TCHAR szItem[BUFSIZE] = {0};
LoadString(g_hInst, IDS_MI_COPY, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
LoadString(g_hInst, IDS_MI_PROPERTIES, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdFirst);
}
所有這些在探測器左窗框中都能很好地工作。如果你想要在觀察中捕捉右擊(即,右窗框),則必須自己做所有的工作。在觀察視窗過程中感覺WM_CONTEXTMENU消息是一種方法。感覺到這個消息之後,你就可以飛快地建立菜單,從資源中裝入模版。另外,如果在觀察類中有一個IShellFolder指針可用,你就可以調用GetUIObjectOf(),使IContextMenu接口幫助你構造要顯示的菜單。然而此時你必須識别那一個窗框調用這個菜單:是左還是右。一般我們認為應該采用建立新菜單的方案。注意,微軟系統資料庫觀察示例中沒有對關聯菜單的支援。
較好的關聯菜單代碼
盡管上面的代碼肯定能工作,但是它并沒有考慮到菜單應該适應檔案對象所有可能的特征。例如,如果檔案夾項設定了SFGAO_CANRENAME位,關聯菜單就應該提供‘重命名’指令(局部地,如果需要)。IContextMenu::QueryContextMenu()方法的uFlags變量可以采用一定數量的值,這在第15章中已經部分地讨論過,現在需要在命名空間擴充中再次回顧它們。
标志 | 描述 |
CMF_EXPLORE | 這個菜單被顯示在探測器樹觀察視窗。添加探測器指令。 |
CMF_RENAME | 檔案對象可以重命名。添加重命名指令。 |
CMF_DEFAULTONLY | 命名空間擴充應該隻加預設項,如果有的話。 |
CMF_NODEFAULT | 命名空間擴充不應該定義任何預設項 |
更精确的代碼可以從前面QueryContextMenu()實作代碼中通過添加一小段代碼獲得。如下代碼,檢測CMF_CANRENAME标志,和添加新指令到菜單。
if(uFlags & CMF_CANRENAME)
{
LoadString(g_hInst, IDS_MI_RENAME, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
}
要處理這個指令,你可以使用SendControlMsg()和FCW_TREE常量直接發送消息到樹觀察。相反,探測器指令的實作,你可以使用ShellExecuteEx(),傳遞PIDL作為檔案對象。
一般情況下,實作QueryContextMenu()應該考慮所有可能的标志,但有時這并不是總有意義。在我們的例子中,要想描述對于視窗編輯應該做什麼就有點問題——我們的腦子裡隻有視窗标題。
連接配接圖示與項
IExtractIcon是一個有用的接口,它使你能把圖示與檔案夾項關聯到一起,但是再一次注意,它僅僅操作顯示在左窗框上的圖示。這與關聯菜單的理由是一樣的:Shell不知道什麼時候客戶觀察需要圖示。顯示在清單觀察中的圖示顯然是由充填清單觀察的代碼确定的。
對于顯示在位址欄和樹觀察節點上的圖示,你需要實作IExtractIcon接口的下面函數:
STDMETHODIMP CExtractIcon::GetIconLocation(UINT uFlags,LPTSTR szIconFile,
UINT cchMax, LPINT piIndex, LPUINT puFlags)
{
*puFlags = GIL_DONTCACHE | GIL_PERINSTANCE;
return S_FALSE;
}
STDMETHODIMP CExtractIcon::Extract(LPCTSTR pszFile, UINT nIconIndex,
HICON* phiconLarge, HICON* phiconSmall, UINT nIconSize)
{
HWND hwnd = m_pPidlMgr->GetData(m_pidl);
if(hwnd == GetDesktopWindow() ||
hwnd == FindWindow(__TEXT("shell_traywnd"), NULL))
{
*phiconLarge = LoadIcon(NULL, MAKEINTRESOURCE(IDI_WINLOGO));
*phiconSmall = LoadIcon(NULL, MAKEINTRESOURCE(IDI_WINLOGO));
return S_OK;
}
*phiconLarge = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_WINVIEW));
*phiconSmall = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_WINVIEW));
return S_OK;
}
除了兩種情況之外,我們都使用相同的圖示:在視窗是桌面,或者任務條時。在這兩種情況下,我們使用類似于Windows商标的圖示。象第9章的例子,任務條的視窗類是shell_traywnd,這是我們使用Spy++搜查時發現的。在命名空間擴充中使用圖示時,我們還應該考慮使用IShellIcon,而不是IExtractIcon,因為它是一個更快地獲得圖示的方法。然而它的能力比較弱,IShellIcon隻輸出一個函數GetIconOf():
HRESULT IShellIcon::GetIconOf(LPCITEMIDLIST pidl,
UINT flags,
LPINT lpIconIndex);
Flags變量起着與IExtractIcon接口的GetIconLocation()方法的第一個變量同樣的作用。合法的值是GIL_FORSHELL 和GIL_OPENICON(見第15章)。這個函數在lpIconIndex參數中傳回索引,是相對于系統圖示圖像清單的。如果傳回的圖示沒在系統圖像清單上,開發者負責在傳回索引之前将其插入,當然應該確定隻插入一次。
IShellIcon的好處是你不需要在每次需要圖示時都建立它的一個執行個體,代之的是在實作它的時候,你自動使Shell請求它傳回給定PIDL的系統圖像清單索引。在整個會話期間,它僅僅運作一個執行個體。就象一種接受PIDL和傳回圖示索引的伺服器一樣工作。
反過來,使用IExtractIcon,你有一個展示出現在Shell檔案夾某些地方的單獨的,個别圖示的接口。也就是說,每次涉及到不同圖示時,你都需要一個新的執行個體。當在樹觀察視窗繪制圖示時,探測器首先搜尋IShellIcon,并且僅當沒有找到它時才通過GetUIObjectOf()求助于IExtractIcon。
安裝命名空間擴充
讨論到這此時,我們已經完成并編譯了我們的命名空間擴充。下一步是安裝和使其适當工作。我們需要作一些操作:
注冊命名空間作為COM伺服器,指定Apartment線程模型,圖示,和擴充名
注冊它成為被認可的擴充,使它也能在Windows NT下工作
定義你的連接配接點到系統。
頭兩點是容易實作的:
REGEDIT4
; 在同一行上寫系統資料庫條目是絕對必要的...
; 注冊伺服器和它的線程模型
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}\InProcServer32]
@= "C:\\WinView\\winview.dll"
"ThreadingModel" = "Apartment"
; 注冊擴充名
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}]
@= "Windows View"
; 注冊圖示
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}\DefaultIcon]
@= "C:\\WinView\\winview.dll,0"
; 在NT下注冊擴充
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ShellExtensions\Appr
ved\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}]
@= "Windows View"
這裡{F778AFE0-2289-11d0-8AEC-00A0C90C9246}是我們的命名空間擴充的CLSID,象在Guid.h中定義的一樣。這個清單的最後三項實際由你自己決定,在這種情況下,我們的擴充不使用檔案做任何操作,是以,對給定的文檔類型,它不能了解。一個更好的想法是使用目錄作為連接配接點,通過建立:
My Windows.{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
檔案夾,你就能獲得下面對結果:
最後,我們決定另外的方法:把擴充放到桌面命名空間中。
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\Desktop
\NameSpace
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
我們簡單地在NameSpace下用擴充的CLSID建立了一個新鍵。對于出現在桌面上的圖示和探測器樹觀察新接點這是充分的。
然而,還有另外的設定也應該儲存:這個檔案夾的預設标志。我們在下面路徑上存儲新的ShellFolder鍵:
HKEY_CLASSES_ROOT
\CLSID
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
\ShellFolder
Attributes條目則被設定為 SFGAO_FOLDER | SFGAO_HASSUBFOLDER:
這正是要讨論ShellFolder鍵的時候。過一會我們将告訴你它們更多的事情。
桌面上的節點
視窗的桌面包含了一些圖示,它們引用系統特殊的檔案夾。用上面的技術,你可以加入具有新标記的系統檔案夾,無論你想要它有何種行為。‘視窗觀察’圖示既不是一個快捷方式,也不是一個直接拷貝到桌面目錄上的程式。它是一個客戶化的系統檔案夾,定位在桌面上。如果我們現在想要打開它,則可得到一個根觀察:
附加資訊标簽
你一定注意到了幾乎所有出現在桌面上的系統檔案夾都有一個工具标簽(我們也稱之為資訊标簽),當滑鼠在其上遊動一會,這個标簽就被顯示。下圖顯示了‘網路上的芳鄰’的情形:
你會感到奇怪,為什麼資訊标簽不同于工具标簽,我們将告訴你:這隻是命名的差異。工具标簽是由‘工具’和‘标簽’組合而成,表示工具的輔助說明。‘資料标簽’,‘标題标簽’和‘資訊标簽’都是同義的。
加一個資訊标簽到我們自己的命名空間擴充上是可能的,在通讀了可用的資料後,我們建議你擷取這個資訊标簽必須使用新的IQueryInfo接口,然而,事實上有更簡單和更可靠的技術。就是在系統資料庫的CLSID鍵下添加一個新值,在我們的例子中是:
HKEY_CLASSES_ROOT
\CLSID
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
值是InfoTip,其内容則是要顯示的串:
我們并沒有找到關于這一點任何知識庫文章和官方資料。隻是簡單地比照‘我的計算機’注冊值——一個桌面命名空間擴充。其它擴充也有這個特征。
附加可删除消息
在開發這個擴充期間,有一點是我們想要從桌面上删除它,并重新安裝。我自動右擊桌面上的這個圖示來搜尋‘删除’指令。但是沒有找到任何東西。沒關系(我想),我把它拖到‘資源回收筒’,還是沒起作用。因為确實要删除它,是以必須手動地幹預系統資料庫。
在上面路徑下的CLSID鍵之前,我檢視了一下PC上存儲的其它系統檔案夾的資訊:‘收件箱’,和‘資源回收筒’。尤其是收件箱項定義了一個值得關注的值串:Removal Message。立即傳回桌面,右擊收件箱圖示。它有一個删除指令,選擇之後,産生包含删除資訊的對話框,這個删除資訊是設定在系統資料庫中的Removal Message值的内容。
使檔案夾可删除
現在我們知道怎樣設定客戶可删除消息,但是怎樣使删除指令在檔案夾的關聯菜單中被允許呢?它們都是使用檔案夾的特征和系統資料庫中的Attributes鍵。為了使檔案夾可删除,隻要通過添加SFGAO_CANDELETE屬性告訴Shell就可以了。我們改變存儲在系統資料庫中的檔案夾屬性,再次注冊這個擴充,改變立即發生了,下圖就說明了最終的結果:
附加檔案夾屬性
不需要更多的研究你就可以通過添加特征條目的其它值實作檔案夾的‘重命名’和‘屬性’這兩個關聯菜單項,如SFGAO_CANRENAME 和 SFGAO_HASPROPSHEET。重命名完全由系統來處理——使用者所要做的就是在保持圖示選中的情況下點選圖示或按下F2鍵。
實作屬性指令需要作更多的工作。所需要的是提供IShellPropSheetExt接口的COM子產品,就像第15張中Shell擴充那樣,在點選标準的‘屬性’項時,系統自動搜尋被點選對象的這個接口。你需要注冊它在Shellex\PropertySheetHandlers鍵下:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
浏覽客戶檔案夾
通過命名空間擴充展示的客戶檔案夾沒必要顯示在‘打開’或‘另存為’通用對話框中。為了能在通用對話框中出現,你需要聲明檔案夾為檔案系統的一部分,因為這些對話框僅僅允許檔案系統的檔案夾顯示。技巧是添加另外兩個SFGAO_XXX常量:SFGAO_BROWSABLE 和 SFGAO_FILESYSTEM。
然而,即使我們在标準打開對話框中顯示了這個檔案夾,也沒有太多的用途,因為這個檔案夾并不響應觸發資訊,絕不進入下一層。還有,這個對話框根本不響應點選和Enter鍵。
有一個接口似乎可用于在通用對話框中顯示客戶檔案夾,使其具有普通檔案夾的能力:ICommDlgBrowser。然而,它是由通用對話框實作的。不幸的是微軟并不支援這個對話框浏覽任何第三方命名空間。為了從客戶檔案夾檢視和選擇項,你需要借助我們在第5章遇到的函數SHBrowseForFolder()。
使這個例子工作
從Web站點上下載下傳和重新編譯了這個例子之後,安裝這個擴充所要做的就是注冊這個控件。必要的設定已經寫進了子產品的DllRegisterServer()函數,在這個生成的DLL上調用regsvr32.exe,然後重新整理探測器,将能使每一件事情都能像描述的那樣正常工作。
解除安裝這個示例
如果你想解除安裝這個例子,最簡單的方法就是在桌面上右擊這個圖示,然後選擇‘删除’。這将從系統資料庫的‘desktop’節點删除其CLSID鍵。如果你願意,也可以通過系統資料庫編輯器來删除:
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\Desktop
\NameSpace
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
從上鍵中删除最後一項,然後傳回到桌面,按F5重新整理螢幕。但是要說明,使用菜單驅動的方法是更好的。在選擇删除時,将有下面資訊出現:
确認後,圖示将從桌面消失。注意,删除過程絕對與系統任何其它檔案夾的删除過程一樣。
命名空間擴充總結
命名空間擴充在Windows95和Windows NT版被引進,而且在以後的版本中其程式設計規則沒有變化。但是,自從Shell4.71之後你就可以寫在兩種觀察模式之間變化的命名空間擴充了:典型的和web觀察的。
典型觀察就是标準觀察,對于此時的檔案夾,系統是通過清單觀察展示的典型觀察。相反,對于Web觀察,它是同樣内容基于HTML的觀察。對于程式員的挑戰不僅要建立這兩種觀察,而且還要使它們可互動。
在編寫命名空間擴充時,你應該考慮加入支援Web觀察的能力。允許使用者在典型觀察和Web觀察之間轉換的方法是探測器的‘觀察 | 作為Web頁’菜單。
這一章剩下的部分,我們将描述兩種技術來組合你的檔案夾與HTML。首先,我們提供關于web觀察的概覽,并計劃在實作web觀察的命名空間擴充時所産生的的設計結果。其次我們展示怎樣使用已存在的Shell觀察結構定制用于顯示普通檔案夾資料的模版。這是由檔案夾關聯菜單上的指令‘定制檔案夾’所允許的特征。
什麼是Web觀察
對于Windows95和NT上的活動桌面和後來的Windows98 ,引進了檔案夾的web觀察。基本上講,這是一個動态HTML頁面,其中包含了作為部件的典型觀察,一些其它小部件如GIF圖像,以及微小的觀察控件。為了給出更形式化的定義,我們首先要解釋:
對于命名空間擴充其‘典型觀察’是用于顯示檔案夾内容的視窗。
現在我們定義web觀察:
對于命名空間擴充其‘web觀察’是顯示動态HTML頁面的浏覽器視窗,其中包含了
典型觀察作為一個部件。
在建立了一個工作的命名空間擴充之後,添加web觀察支援就是要輸出它的觀察視窗(典型觀察)作為一個嵌入到動态HTML頁面上的部件。此外,你必需準備在兩個觀察之間轉換,而且将來如果不夠,還要考慮觀察的數目可能多于兩個。典型的Shell觀察已經提供了同一資料的四種觀察:大小圖示方式,細節和清單方式,這些隻是用于表述普通資料控件——清單觀察的能力——不是确切地由提供檔案夾行為的代碼給出的。Web觀察有點不同,它實際要求不同的視窗和部件。這些新行為參與者必須适合已存在的規則并與環境很好地結合。
從命名空間擴充的觀點上看,使用web觀察要求兩個新的接口:IShellView2和 IPersistFolder2。現在這些僅僅在Windows98以及後來的版本中才支援。Web允許的命名空間擴充具有我們到現在為止所看到的所有特征,以及支援‘觀察 | Web頁面’菜單指令的能力。在這個指令激活時,期望擴充顯示和以前相同的内容,但是是使用HTML模版來顯示。在兩種觀察之間轉換的能力暴露出一些附加的問題,要稍微修改一下命名空間擴充的整體結構。開始寫作時這方面的資料剛開始出現,最好的是引用平台SDK資料。這一節我們就給出一些關于web觀察的注釋。
Shell觀察ID
如果你檢視最近的shlobj.h頭檔案源碼,就會發現下面的定義:
typedef GUID SHELLVIEWID;
看上去并不熟悉,但是這引出命名空間擴充中的一個概念:Shell觀察ID。這是辨別一種Shell觀察的唯一号碼,在絕大多數場合它等同于實作這個擴充的COM對象CLSID。
這個新辨別符由IShellView2接口的函數使用,以在可能的觀察之間轉換。實際上,當你在浏覽器的菜單或工具條上選擇或放棄Web觀察選項時,探測器用不同的Shell觀察ID調用IShellView2::CreateViewWindow2(),産生典型的活web觀察。
預設的觀察
在内部,CreateViewWindow2()必須差別和實際建立不同的觀察視窗。值得關注的是它不能傳回不同的視窗Handle到探測器。換句話說,探測器不與特定的觀察交談,無論它是清單觀察還是Web浏覽控件的執行個體,代之,它總是涉及相同的視窗,這個視窗稱為預設觀察。這是一個SHELLDLL_DefView類視窗。
下圖顯示了同一個檔案夾通過典型或web觀察調用時兩個視窗之間的差别。注意預設觀察在二者中都出現:
左圖是典型觀察,右圖是同一個檔案夾的web觀察。Internet Explorer_Server是這個視窗的Web浏覽器控件的類名。我們可以推論,Web允許的命名空間擴充必須建立web觀察視窗作為其典型觀察視窗的姊妹。下圖顯示了探測器與觀察之間的關系:
開始預設觀察應該指定一個可用的觀察(一般是典型觀察)。而後,每當探測器調用CreateViewWindow2()時,命名空間擴充都将獲得Shell觀察ID,并在預設觀察視窗内顯示由這個ID辨別特定的觀察。
IShellView2接口的新函數
在IShellView2中有四個新函數:
函數 | 描述 |
CreateViewWindow2() | 允許在不同的觀察之間轉換,包括web觀察和典型觀察。 |
GetView() | 傳回由定義常量指定觀察的Shell觀察ID,一般情況下僅有兩種可能性:預設觀察SV2GV_DEFAULTVIEW,和目前觀察SV2GV_CURRENTVIEW。 |
HandleRename() | 允許改變給定項的PIDL。它接收PIDL,并用新的一個置換它。 |
SelectAndPositionItem() | 這個函數接受PIDL,某些标志,和指向POINT結構的指針。它的用途是:把選擇項放到觀察的任何地方,對于檔案夾的客戶化的HTML觀察,這是合理的。 |
CreateViewWindow2()方法的輸入僅有一個指向SV2CVW2_PARAMS結構的指針,其定義如下:
typedef struct _SV2CVW2_PARAMS
{
DWORD cbSize;
IShellView* psvPrev;
FOLDERSETTINGS const* pfs;
IShellBrowser* psbOwner;
RECT* prcView;
SHELLVIEWID const* pvid;
HWND hwndView;
} SV2CVW2_PARAMS;
讓我們來比較一下它與IShellView::CreateViewWindow()。
HRESULT CreateViewWindow(LPSHELLVIEW pPrevView,
LPCFOLDERSETTINGS lpfs,
LPSHELLBROWSER psb,
LPRECT prcView,
HWND* phWnd);
如上所示,結構中實際包含了相同類型的變量,以及一個Shell觀察ID和典型的尺寸成員。顯然CreateViewWindow2()是CreateViewWindow()的重定義,并且引進了對Web觀察的支援,除了cbSize字段,用于微軟的内部目地以外,Shell觀察ID是唯一的變化。
IPersistFolder2接口的新特點
IPersistFolder2接口通過增加一個GetCurFolder()新函數擴充了IPersistFolder。它的原型為:
HRESULT IPersistFolder2::GetCurFolder(LPITEMIDLIST* ppidl);
它使用一個‘輸出’參數來傳回目前對象的PIDL,以使對于外部世界,這個目前被觀察檔案夾的PIDL是可用的。實際上,它是在命名空間擴充初始加載時由IPersistFolder::Initialize()方法傳遞的同一個PIDL。
怎樣構造Web觀察
如果你想要處理給定檔案夾的web觀察,就必須至少實作下面三步:
定義預設觀察對象作為其它觀察對象的容器
定義典型觀察對象用檔案夾項提供标準的觀察
定義web觀察對象,它包括典型觀察作為部件
你的預設觀察應該能根據接收的Shell觀察ID傳回典型觀察或web觀察對象。此外,Web觀察必須包含一個Web浏覽控件,以使其導出動态HTML頁面提供的模版。這個模版應該包含典型觀察對象作為ActiveX部件。如果混淆了的話,下圖能幫助你搞清楚這個問題:
接觸典型觀察對象
在上圖中,web觀察對象含有一個Web浏覽控件,這個控件指向一個動态HTML頁面。依次這個頁面通過CLSID含有一個典型觀察作為其一個部件。事情是web觀察需能驅動典型觀察對象,例如,它需要通知這個觀察顯示正确的檔案夾内容。為了管理,web觀察對象需要通過調用IWebBrowser2::get_Document()從Web浏覽器中獲得‘文檔’特征。
HRESULT IWebBrowser2::get_Document(IDispatch** ppDisp);
這個文檔對象是動态HTML對象模型的根。在一個ActiveX文檔駐留到ActiveX容器,如Web浏覽器中時,文檔特征傳回其指向文檔對象模型的入口(IE4.x把HTML檔案作為ActiveX文檔)。通過枚舉頁面中的<OBJECT>标記集,你應該能夠獲得典型觀察對象的IDispatch接口指針。典型觀察通過<OBJECT>标記方式嵌入到動态HTML模版中,形式如下:
<object classid="clsid:{...}"></object>
由于web觀察對象知道典型觀察對象的CLSID,是以能夠容易确定哪一個枚舉對象是正确的。一旦取得了IDispatch指針,就有了使web觀察和典型觀察通訊的方法。
Web觀察模版
Web觀察由HTML頁面特征化,這個頁面可以駐留在任何我們能找到的地方:可以是與實作這個擴充的DLL同路徑的檔案,或者是存儲在系統檔案夾下的檔案。在第5章中我們看到過,有一個系統檔案夾是為模版準備的,過一會我們還會遇到它。一般情況,你必須給出不同的檔案,如果這個檔案消失,可能引起問題。
對于web觀察,一個好的方案是把HTML檔案嵌入到擴充的資源中,然後建立本地拷貝,或直接采用‘res://’協定讀出這個資源,這個URL協定使你能從可執行檔案的資源直接加載HTML檔案(一般情況下,任何Win32資源)。使用這個方法,你就可以嵌入HTML模版到資源中,是以而忽略任何問題。例如,如果命名空間擴充的資源中有下面一行代碼:
MYTEMP.HTT HTML "myTemp.htt"
你就可以請求Web浏覽器導航下面的URL:
res://<dll_path>/MYTEMP.HTT
在這裡還有一個潛在的問題:我們怎樣來處理所有雜項如GIF,控件,和腳本程式。簡單的規則是每一件東西都可以通過嵌入到資源中的不同檔案名加以識别,并且可以通過res://協定在HTML頁中進行引用。
Res://協定也在VC++ 6以後的MFC各版本的鈎子下被使用,用來實作一些 CHtmlView類,它們是對WebBrowser的一種封裝。
觸發事件
除非你的命名空間擴充是非常不尋常的,否則,使用者界面總有一些事情需要選擇。這就是典型觀察和web觀察需要通訊的一般情況。通過HTML,這些事情如下解決。典型觀察嵌入成為觸發事件的部件——設為SelectionChanged。對于web觀察,定義在模版中的代碼解釋這些通知,并作出适當的響應。
從定制到客戶化檔案夾
Web觀察是以定制方式展示檔案夾的方法。改變給定檔案夾展示方法的人是程式員,而且一旦安裝了擴充,使用者僅僅可以在典型與web觀察之間轉換。
通常檔案型檔案夾由系統提供web觀察,興趣所在是這些觀察可以被客戶化。換言之,隻要你從正常的目錄轉換到web觀察,就能根本上改變這個web觀察的模版。這個過程叫做檔案夾客戶化。
如果在自己的擴充中希望檔案夾客戶化,簡單地保持它所驅動的HTML模版可視就行了。否則可以通過把它們潛入到可執行檔案的資源中來隐藏它們,并使用res://協定恢複它們。
檔案夾客戶化本身并不是命名空間擴充,但是它們可以被看作是超簡單的模仿,實際上這可能有點滑稽:有時,客戶化檔案夾可能實際作為控制台來表示客戶行為的資料,然而不要忘了,客戶化檔案夾是基于簡單的HTML模版形成的,完全依賴于檔案型檔案夾的web觀察擴充的存在和工作。
檔案夾客戶化
Shell版本4.71以後,任何使用者都有機會客戶化每一個檔案夾的外觀。如果你在檔案夾上右擊,并選擇‘客戶化檔案夾’項,則有下圖出現:
這個向導非常簡單,并最終用folder.htt打開記事本。.htt檔案是一個HTML檔案,它定義了檔案夾的外觀。如果接收并儲存标準檔案内容,這個觀察有如下外觀:
這個頁面展示的結構如下圖所述:
正如所想象的,這個頁不是靜态的:其大部分内容都動态決定。目錄名,圖示,檔案資訊,以及預覽全部都是運作時生成的。尤其是頁面嵌入的三個ActiveX控件:一個是抽取圖示,一個是提供目前選擇檔案的預覽(僅支援幾種類型),再有就是實際檔案清單。相反,目錄名由一個宏%THISDIRNAME%給出,它在運作時展開。
預設模版
作為客戶化過程的結束,每一個檔案夾都含有一個隐藏的folder.htt。對于檔案夾查詢和檔案夾本身的行為,這是合理的,向導拷貝到指定目錄下的僅僅是預設的模版。它們遵從上面顯示的外觀。你可以通過塗改,引進任何HTML頁元素:幀,表,圖像,腳本,Java小程式等重寫它。
預設模版來自Windows\Web目錄。在那裡存有所有标準的HTML模版。如果打開它,你能發現某些系統檔案夾的外觀稍有不同:‘我的計算機’(mycomp.htt),‘列印機’ (printers.htt),‘控制闆’ (controlp.htt),桌面 (deskmvr.htt),以及‘頁面模版’(safemode.htt)。
從這裡不難看出folder.htt嚴格的并不是所有檔案夾的模版,我們可以看一下,在完成了客戶化向導操作後檔案夾模版變成什麼。
Desktop.ini檔案
初始時向導在檔案夾下建立兩個隐藏檔案folder.htt 和 desktop.ini。前者我們前面已經講到了,是以我們主要讨論後者。對于我們,Desktop.ini不是新名字,因為在這一章中已經遇到過它了,即讨論命名空間連接配接點的時候。
一般,Desktop.ini是一個基于檔案夾的資訊檔案,探測器或程式員添加需要這個檔案夾記住的資訊,下面就是這個檔案的典型内容:
[ExtShellFolderViews]
Default={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{5984FFE0-28D4-11CF-AE66-08002B2E1262}={5984FFE0-28D4-11CF-AE66-08002B2E1262}
[{5984FFE0-28D4-11CF-AE66-08002B2E1262}]
PersistMoniker=file://folder.htt
[.ShellClassInfo]
ConfirmFileOp=0
各個條目的實際意義仍然是微軟聲明的,但是有一樣是一定的:如果你改變了這個.htt檔案的名字,探測器将搜尋新的一個。CLSID必須一緻于裝入這個HTT檔案的子產品。這個檔案的内容并不固定。例如,在檔案夾屬性對話框中選中‘Enable thumbnail’觀察框:
desktop.ini的内容将變為:
[ExtShellFolderViews]
Default={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{5984FFE0-28D4-11CF-AE66-08002B2E1262}={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{8BEBB290-52D0-11d0-B7F4-00C04FD706EC}={8BEBB290-52D0-11d0-B7F4-00C04FD706EC}
[{5984FFE0-28D4-11CF-AE66-08002B2E1262}]
PersistMoniker=file://folder.htt
[.ShellClassInfo]
ConfirmFileOp=0
[{8BEBB290-52D0-11d0-B7F4-00C04FD706EC}]
MenuName=T&humbnails
ToolTipText=T&humbnails
HelpText=Displays items using thumbnail view.
Attributes=0x60000000
此外,在‘觀察’菜單中出現‘Thumbnails’菜單項。這個項設定了一個新觀察,象下圖顯示的一樣:
在web觀察打開時,探測器搜尋目前目錄查找在desktop.ini檔案中PersistMoniker條目指定的檔案。在失敗的情況下,觀察被重置。你可以給定任何具有.htt擴充名的檔案。奇怪的是,如果使用另一個擴充名——如.htz——你可能得到另一個效果:
目錄名——宏%THISDIRNAME%——沒有被展開,如果模版檔案的擴充名不是.htt。即使你簡單地重命名檔案,而沒有觸及它的源碼,也是如此。
注意,你可以指定任何協定來通路.htt檔案,不必象desktop.ini表示的那樣‘file://’。例如,可以是‘http://’指定内網中的一個檔案。
建立新模版
作為檔案夾客戶化的實踐,我們看一段置換标準folder.htt模闆的完整的客戶化代碼,下圖說明了這個HTT檔案的結構:
頁面分成兩個部分。上部通過2x2表的方法被進一步劃分成四塊,GIF圖像和目錄名在第一行上,位圖按鈕和一條水準線在第二行上。
下部整個由檔案清單或某些其它資訊占用。重要的是下部這兩種顯示是互相獨立的。位圖按鈕根據使用者點選決定顯示哪種内容。實際上這個按鈕控制檔案清單的顯示與關閉。你并不總需要包含檔案清單,而且如果必要完全能夠隐藏檔案夾的實際内容,獲得看上去象命名空間擴充所操作的結果。我們所使用的這個模闆除了幾個動态HTML技術之外,并沒有什麼特别之處。詳細一點講,我們采用了:
圖像褪色
3D繪圖效果
事件處理
熱跟蹤文字(當滑鼠通過時改變顔色)
下圖說明最終獲得的結果:
現在看一下這個wrox.htt的源代碼:
<html>
<head>
<style>
.Title {font-Size: 38; font-Family: Verdana; font-Weight: bold; color: #808080;
text-align: center;filter:Shadow(Color=#909090, Direction=135);}
.Small {font-Size: 10; font-Family: Verdana;}
.BookInfo {font-Size: 16; font-Family: Verdana;}
.HiliteSmall {font-Size: 10; font-Family: Verdana; color=red; cursor: hand;}
.Panel {background-color: #C0C0C0;}
.Fade {filter: alpha(opacity=0);}
</style>
</head>
<script language="JScript">
var strHide = "Hide the file list below.";
var strShow = "Show below the file list.";
// 激活褪色
function init()
{
logo.flashTimer = setInterval("fade()", 100);
}
// 實際改變圖像的不透明性産生褪色效果
function fade()
{
if (logo.filters.alpha.opacity < 100)
logo.filters.alpha.opacity = logo.filters.alpha.opacity + 10;
else
clearInterval(logo.flashTimer);
}
// 當滑鼠退出頁面部件時...
function mouseout()
{
obj = event.srcElement
if(obj.id == "msg")
obj.className = "Small";
}
// 當滑鼠在頁面部件上時...
function mouseover()
{
obj = event.srcElement
if(obj.id == "msg")
obj.className = "HiliteSmall";
}
// 在點選滑鼠時...
function mouseclk()
{
// 如果事件不是由一定元素發起的...
if(event.srcElement.id != "toggle" && event.srcElement.id != "msg")
{
return;
}
// 開/關檔案清單
if(toggle.visible == 1)
{
toggle.src = "closed.gif";
msg.innerHTML = toggle.outerHTML + strShow;
toggle.visible = 0;
FileList.width = 1;
FileList.height = 1;
book.style.display = "";
}
else
{
toggle.src = "opened.gif";
msg.innerHTML = toggle.outerHTML + strHide;
toggle.visible = 1;
FileList.width = "100%";
FileList.height = 200;
book.style.display = "none";
}
}
</script>
<body onload="init()" onmouseover="mouseover()"
onmouseout="mouseout()" onclick="mouseclk()">
<table><tr>
<td width=20%>
<img src="Wroxlogo.gif" width=80 class="fade" id="logo">
</td>
<td class="Title">%THISDIRNAME%</td>
</tr>
<tr>
<td class="Small" id="msg">
<img src="opened.gif" visible=1 align=left
alt="Toggles the file list" id="toggle">
</img>
Hide the file list below.
</td>
<td><hr></td>
</tr>
</table>
<br>
<div class="Panel">
<div id="book" style="display:none">
<img src="1843.gif" align=left></img>
<span class="BookInfo">
<b>Visual C++ Windows Shell Programming</b><br>
<i>Dino Esposito</i><br>1-861001-84-3<br>Wrox Press, 1998
</span>
</div>
<object id="FileList"
width=100% height=200
classid="clsid:1820FED0-473E-11D0-A96C-00C04FD705A2">
</object>
</div>
</body>
</html>
檔案清單作為FileList名的ActivX控件被輸出。它有自己的屬性,方法和事件集,在這個例子中我們完全不管它,但是标準的folder.htt檔案可以考慮使用這些屬性,這個對象的資料在平台SDK中,可以搜尋名為WebViewFolderContents的對象。
雖然這本書不是關于動态HTML的,但是這段代碼中有幾個使用了的技術需要提一下。褪色效果通過對<IMG>标記配置設定一個特定風格來獲得,它定義了一個不透明系數,并且每100毫秒增加一次。陰影文字不是位圖,而是使用某種繪圖效果繪制的串,它隻是一種風格,其參數要求顔色和光照方向。
通過配置設定頁面元素的ID,你可以用腳本代碼非常好地控制它們。也就是說,你也可以感覺和處理特定元素的事件——如,在特定位圖上的點選。而跟蹤效果正是這種事件的感覺過程。在滑鼠進入或退出元素區域時,為了增強亮度,我們簡單地寫了兩個過程來改變文字的顔色(更确切地說是使用不同的風格類)。
通過命名空間擴充來內建應用
作為命名空間擴充這一章的結束,我們來看一下實際應用駐留在Windows探測器中究竟有什麼好處。坦白地說,沒有要學習的新東西:命名空間擴充是一個允許你定制檔案夾的子產品,而且我們已經看到怎樣建立特殊檔案夾并把它們與命名空間擴充連接配接到一起。所缺少的是應用。關鍵是觀察對象。一個觀察對象是一個視窗,有時它可以是一個對話框模闆。基于此,任何基于對話框的應用都符合嵌入進定制檔案夾的條件。如果有時間修改工具條和菜單,就更有理由這樣講了。這種命名空間擴充是簡單的,因為你不用考慮項,PIDLs,圖示,關聯菜單等。所有功能都從應用導出,它駐留在觀察中,這個命名空間擴充必須提供的僅是檔案夾管理器,以及建立觀察的基本行為。
URL檔案夾舉例
這種情況的最小示例是URL檔案夾的例子,你可以從我們的web站點下載下傳這個例子。它是含有一個按鈕的對話框,在Wrox出版社web站點打開浏覽器視窗。然而,它的所有控件都是在客戶視窗過程中進行處理的,因而也就成了可嵌入應用的原型。下圖顯示了它的外貌,通過運作下載下傳所包含的.reg來安裝它:
小結
在這一章中,我們讨論了命名空間擴充的各個方面,我們的觀點是,命名空間擴充是Shell程式設計非常本質的東西:它允許你的代碼緊密結合于探測器,并且允許在各個層次上進行客戶化。
從這一點上看,命名空間擴充是以也是相當複雜的。這并不是由于它們有許多内部技術,當然這是一方面,而是由于幾方面因素的組合。其中必須實作一定數量的接口,以及所有使子產品正常工作所必須的接口(和這些接口的函數),嚴格地講你可能并不需要支援這些接口。Windows系統的持續更新,以及資料的貧乏要完成這個工作是很困難的。
再有,凡是有點價值和值得讨論的資料通常都是不完整的,而且缺少的都是主要部分的内容,并且還沒有很好的舉例說明。僅僅是在建立了Internet客戶SDK之後才有了關于非根擴充的值得關注和有意義的示例。
這一章我們覆寫了:
使命名空間擴充正常工作的所有必要接口
怎樣建立和管理PIDLs
怎樣映射所有理論到實際工作
怎樣修改探測器的使用者界面
怎樣使用無資料說明的屬性豐富命名空間擴充,例如資訊标簽和可删除消息
Web觀察擴充的概覽
怎樣在探測器中駐留應用的提示
檔案夾客戶化和HTML模闆檔案
采用動态HTML獲得檔案夾的互動
最後的思考
這裡是這本書的結尾,在這16章中,我們試圖弄清相關于Windows Shell的Win32程式設計方面的所有科目。我們成功了嗎?某些人也提出這個類似的問題,然而回答是即使是專家,也隻有在犯過所有可能的錯誤之後才行。使用Windows Shell,在近兩年已經花費我們大量的時間,有幾次我都認為我面對的是世界上最大的系統代碼bug。有很多次,我自己解決了bug ,在這些筆記中我收集了所有我遇到的問題和所有我找到的解答。
毫無疑問用Shell工作是艱難的,它是一個COM與API的混合,C和C++ 的混合,并且有一些資料僅僅是為VB而寫的。當然,資料雖不太多,也并不貧乏,是以,這也使我們對含混的描述以及複雜行為的組合效果增強了判斷能力。
我們确實希望你能喜歡這本書,從頭至尾閱讀它。我想做的最後一件事就是通過解釋的方式回答兩個評論家提出的問題,這也可能是在你腦海中纏繞的問題:為什麼說這本書是C和C++ 以及幾個ATL而非MFC的混合?我的目标是忠實于Albert Einstein的形式語句,他推薦說任何事情都應該保持盡可能的簡單,而不是比較簡單。MFC有其自己的特點。為了可靠,你必須知道你在做什麼。在底層的表述代碼允許我們在各個層面上接觸所有要求的步驟。我不知道你的感覺怎樣,但是,我相信,一旦你領會了其中的技術——無論它是什麼——你都能面對任何在這之上構造的東西。
事情并沒有在這裡停止。總有新的目标需要實作。Shell是進化的,當然我也希望這本書跟着進化。我十歲的時候就開始在TV上觀看UFO和火星人。‘Windows 2000 企業版’更可能作為太空梭的名字而不是作業系統的名字,然而誰能想象十年後的作業系統是什麼樣子,2000年的時候世界将全部是自動的世界了(很不幸,現在還沒有實作)。
Windows 2000 被吹捧為作業系統之父:完全的即插即用,象光速一樣快,思維驅動裝置。是不是都是真的,我們可以說Windows2000 給我們帶來了許多要學習的新東西,增強的性能,排除了某些bug,以及其它的變化。有些事情不可避免地影響到Shell。是以調侃一下,準備在下一個千年閱讀更多的知識。
