1. 程式人生 > >Windows Shell程式設計-第十六章.名稱空間擴充套件

Windows Shell程式設計-第十六章.名稱空間擴充套件

第十六章 名稱空間擴充套件

         探測器使用層次結構表述形成系統的許多物件——檔案,資料夾,印表機,網路物件等等。這些物件組合定義了一個名稱空間,這是一個封閉的符號或名字集合,其中任何給定的符號或名字都能成功地被喚醒。在名稱空間中解析一個名字就是成功地連線給定的名字到某個它所表述的實際資訊。

         探測器仔細地把所有這些物件收集到一起,與它們通訊,把它們的內容顯示在典型的兩窗框視窗中,樹狀檢視在左側,列表檢視在右側。

         我們實際所關注的是探測器是否支援插入程式碼到它的結構中並增加全新定製物件的介面。事實上,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容器)的選單都劃分成六個組,每一組都可以包含許多不同選單,分組是:

組名

位置

控制者

檔案

0

探測器(容器)

編輯

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);