第十五章 SHELL擴展程序員
談到Windows Shell編程,Shell擴展是最重要的科目之一,絕大多數商業應用的最酷特徵的都是經過Shell擴展實現的,並且有許多顯著的系統特徵實際都是插入了擴展代碼。Shell擴展尤爲使人激動的是它容許你把你的應用做爲Shell的一部分來處理。shell
Shell擴展的另外一個好處是微軟正在使它變得更聰明,例如,‘查找’菜單,從Windows95 到Windows98 一直是經過Shell擴展加強的,並且增長了新條目。還有,出如今文檔關聯菜單上的位圖項也是使用Shell擴展增長的。編程
Shell擴展不只是構建增長Shell功能模塊的重要手段,並且也是使應用得到有力的Shell特徵的重要方法。在前面各章中,咱們討論了系統集成方面Win32應用程序應該作的工做。咱們探討了關聯菜單,圖標,和幾個其它方面技術。然而,這些都是靜態和肯定的。你能夠設置或刪除它們,然而,這些就是你所能作的所有:在這之間你不能作任何事情。所以,通向徹底融入Windows的應用最後一步是要考慮編寫一個或多個Shell擴展的可能性。注意,我說的「可能性」,事實上儘管Shell擴展是與Shell通信的有力而且是靈活的方法,可是它並非你和你的程序必須作的。小程序
在這一章中,咱們將探討全部Shell擴展的編程技術,而且提供某些有意義的示例,主要方向是:windows
Shell擴展是什麼,怎樣與它們一同工做api
用C++ 和ATL怎樣寫Shell擴展數組
Shell擴展的排錯方法緩存
使用Shell擴展定製關聯菜單,圖標,和屬性安全
這章的最後部分將專一於文件觀察器,嚴格地說,它們並非Shell擴展,可是它們有相似的內部結構。文件觀察器是一個程序模塊,它可使你能快速預覽給定類型的文檔而不須要藉助創建和管理那種類型文件的應用。文件觀察器一般與關聯菜單的‘快速觀察’項關聯。服務器
Shell擴展:類型和提示
Shell擴展是一個進程內COM服務器,它在探測器須要時被加載。Shell擴展不是一個全新的概念,它只比Wondows3.1的文件管理器外掛多了一點點東西。然而,Shell擴展使用了COM體系結構而不是DLL函數集,而且給出更普遍的功能範圍。
什麼是Shell擴展
正象上面提到的,Shell擴展是實現COM接口的進程內COM服務器。你須要編寫模塊,註冊它到註冊表,並運行探測器窗口實例來測試它。沒必要着急知道何時,怎樣或由誰來調用它——假若你正確地註冊了它,這些是自動發生的。Shell擴展是DLL,能夠放在PC的任何地方。就象任何其它COM服務器同樣,它輸出四個全程函數,經過這些函數,客戶端模塊能夠識別和鏈接到這個服務器:
DllGetClassObject()
DllCanUnloadNow()
DllRegisterServer()
DllUnregisterServer()
除此以外,Shell擴展還須要提供一般COM的一些接口,如類工廠和IUnknown接口的實現。最後它還必須實現須要與Shell交互的接口。
調用Shell擴展
有必定數量的探測器可識別事件是可經由客戶模塊定製的,例子是探測器顯示關聯菜單或屬性頁,繪製圖標或拖拽文件操做,也就是說,在執行一種文檔的特殊任務時,探測器查找註冊的用戶模塊,若是找到,則鏈接這個模塊並調用要求的接口方法。這個關係看上去有點象Windows初級編程所描述的回調機理。回調是預約義原型的函數(一般有推薦的行爲),服務器模塊將調用這個回調函數以使客戶能夠插入響應給定的事件。Windows API的枚舉函數EnumWindows()就是一個極好的例子。對於Shell擴展所發生的情形概念上與此徹底相似。
文件管理器的外掛
文件管理器的外掛正好依賴於回調函數,在加載時,文件管理器掃描它的winfile.ini文件查找‘外掛’節的DLL名:
[AddOns]
MyExtension=C:/WINDOWS/SYSTEM/FMEXT.DLL
在這個DLL中文件管理器但願找到FMExtensionProc()函數,其原型爲:
LRESULT CALLBACK FMExtensionProc(HWND hwnd, WORD wMsg, LPARAM lParam);
此時,管理器開始發送消息到這個函數。經過編寫這樣一個函數,你就可以添加新工具條按鈕,被通知選中狀態,修改菜單,和做其它操做。若是你願意,能夠參考Internet客戶端SDK資料。
從文件管理器的外掛到Shell擴展
咱們已經有了文件管理器外掛導出操做的概念,如今能夠把這個概念轉換到Shell擴展。這裏主要的結構差別是:
代替單一回調函數的是COM接口
代替INI文件的是一批註冊鍵和值,它們關聯到擴展的文件類型
代替簡單DLL的是COM服務器
因此,儘管有一些無能否認的相似性,文件管理器的外掛與Shell擴展是兩個根本不一樣的概念。技術範圍已經改變:文件管理器外掛是應用爲中心的,信息交換不多考慮單個文件,而且不識別文件類型。Shell擴展分別施加於每一種文件類型——它們是爲這種活動方法而專門設計。
探測器怎樣導入Shell擴展
爲了理解探測器與Shell擴展之間的交互做用,讓咱們調查一個實際狀況。在這個工做完成後你就能清楚地理解這些操做怎樣互相做用,以及爲何Shell擴展要這樣設計。
咱們前面提到過,在進一步處理特定任務集以前,探測器在註冊表的某個地方尋找註冊模塊。它裝入找到的全部擴展,而且調用它們的方法。爲了得到必定的行爲,只需適當地註冊模塊。要禁止它就要註銷這個模塊。
要探查的註冊表確切路徑和擴展的編程接口能夠各不相同,這依賴於探測器觸發調用所引發的事件。
顯示關聯菜單
看一個典型的例子:顯示特定文件類型——位圖(bitmap)的關聯菜單。用戶在Shell觀察下右擊BMP類型文件時這個過程啓動。關聯菜單由不一樣的項目組構成,首先是系統標準項如‘拷貝’,‘剪切’,‘創建快捷方式’和‘屬性’。而後是文檔特有的動詞,這是靜態附加的。再有就是全部文件附加的通用動詞,無論是什麼類型的文件都有這些項。第四組是來自關聯菜單Shell擴展的項,這是爲特定類型文件而註冊的擴展,此時是位圖文件。
當探測器創建彈出菜單時,它啓動全部附加的標準項,和每個註冊表中的項,而後它在相關文件類型的ShellEx鍵下查看(若是存在),搜索ContextMenuHandlers子鍵。對於BMP,其形式爲:
HKEY_CLASSES_ROOT
/Paint.Picture
/ShellEx
/ContextMenuHandlers
位圖的主鍵是Paint.Picture,微軟的Paint是一個管理位圖的程序。這是默認的,除非你安裝了不一樣的圖像軟件。
在ContextMenuHandlers鍵下,默認值包含實現擴展的COM 服務器的CLSID。知道了這個CLSID後。探測器模塊裝入它到本身的內存空間。這就完成了服務器實例的創建,而且查詢擴展所要求的接口。對於關聯菜單,接口是IContextMenu,這個接口包含了添加新菜單項的方法,恢復在狀態條上顯示的描述串,和執行響應用戶點擊的一些代碼。
其工做過程是:探測器首先喚醒IContextMenu::QueryContextMenu(),來請求模塊添加新菜單項。每當新菜單項被選中,探測器都調用GetCommandString()來獲取顯示在狀態條上的描述。最後,當有點擊發生在客戶菜單項上時,運行InvokeCommand()來提供運行時的行爲。這些由探測器喚醒的函數能夠提供在Shell中定製菜單項的手段,固然還須要嚴格地按規定註冊。後面咱們將深刻的研究這些方法。
Shell擴展的類型
咱們反覆提到Shell擴展是在Shell響應特定事件集時被裝入的。所以,有固定數量的Shell擴展,即有輸出不一樣函數的COM接口集來影響特殊的狀況。顯示關聯菜單不一樣於繪製圖標,或顯示屬性對話框,因此不一樣的COM接口作不一樣的工做也就不奇怪了。
Shell擴展的類型是:
Shell擴展 |
接口 |
描述 |
關聯菜單 |
IContextMenu |
容許添加新項到Shell對象的關聯菜單 |
右鍵拖拽 |
IContextMenu |
容許添加新項顯示在右鍵拖拽文件後的關聯菜單上 |
Shell圖標 |
IExtractIcon |
能夠在運行時決定在一個文件類中給定文件應該顯示的圖標 |
屬性頁 |
IShellPropSheetExt |
能夠附加屬性頁到文件類的屬性對話框,對控制板小程序也能工做 |
文件鉤子 |
ICopyHook |
能夠控制任何經過Shell的文件操做。在容許或拒絕時不需告知成功或失敗。 |
左鍵拖拽 |
IDropTarget |
能夠決定在Shell中當對象被拖動(使用鼠標左鍵)到另外一個之上時須要作什麼 |
剪裁板 |
IDataObject |
能夠定義對象怎樣拷貝到剪裁板或怎樣從剪裁板抽取對象 |
編寫Shell擴展
編寫Shell擴展就如同編寫進程內COM服務器同樣,這沒有什麼可奇怪的。你必須提供基本的COM素材,實現接口,適當地註冊服務器,以及隨後的測試和排錯。與任何開發過的其它COM模塊同樣,其中含有大量的重複且不多改動的代碼,這些代碼自己已經封裝在某些C++ 類中。所以咱們能夠預知下一步將要幹什麼。
使用ATL
咱們建議使用ATL做爲開發Shell擴展的工具,畢竟,如今的ATL是C++ 開發COM服務器最好的工具,並且Shell擴展自己就是ATL結構的。微軟活動模版庫是特別設計用於簡化開發COM模塊的,並且遠比MFC先進。
第一個Shell擴展
如今是咱們編寫Shell擴展的時候了。Shell擴展實際是至關簡單的對象,就象開發玩具同樣,即便是頭一個要開發的,也是如此。咱們將從完成前一章的Windows元文件和加強元文件的例子開始。目標是展現怎樣添加客戶頁面到WMF和EMF文件的屬性對話框。
添加屬性頁
直接在屬性頁預覽元文件是否是更好一點。確實,你能夠從文件夾的‘觀察 | 做爲Web頁面’的選項打開所選擇的文件進行預覽,可是,若是你不知道或不想要這個觀察時會怎麼樣。此外,若是你還運行在Windows95或NT上,Shell沒有更新,會怎麼樣。固然,答案是屬性頁的Shell擴展。它與其它任何Shell擴展同樣,都能在IE4.0上工做。
要實現哪些接口
經過ATL COM AppWizard生成ATL代碼以後,所須要解決的問題是:添加屬性頁到‘屬性’對話框須要實現哪些接口。事實上有兩個接口:IShellPropSheetExt和IShellExtInit。頭一個提供添加頁的方法,然後一個仔細的初始化和創建Shell與擴展之間的鏈接。二者都在shlobj.h中定義。
IShellPropSheetExt請求使用API函數創建新的屬性頁,這涉及到通用控件,然後這個頁經過回調函數傳遞給Shell。也就是說,當調用IShellPropSheetExt方法時,Shell傳遞了一個指向函數的指針,這個函數由擴展回調,將頁面做爲變量。這個接口有兩個方法,其中一個在絕大多數場合都不須要實現。
單一方法的IShellExtInit接收在Shell中選中的文件(或文件組)的名字,並使它成爲可用的模塊。可使用任何技術來存儲這些名字,而典型的是使用成員變量。Shell擴展的初始化是一個過程,可能對不一樣類型的擴展有至關的變化,因此使這個機理通用是關鍵所在。
Shell擴展的初始化
咱們須要花費一點時間來討論Shell擴展怎樣初始化的問題。在這裏‘初始化’意指探測器調用擴展,傳遞正確的變量所遵循的過程。基本上,初始化能夠取三種形式之一:沒必要初始化,經由IShellExtInit初始化,和經由IPersistFile初始化。初始化使用的方法依賴於Shell擴展自己的本質。
下表給出各類類型擴展得到初始化的方法(參考前面的Shell擴展類型表)。
初始化 |
應用於 |
描述 |
無須初始化 |
文件鉤子,剪裁板 |
Shell擴展不要求任何初始化過程 |
經IShellExtInit初始化 |
關聯菜單,屬性頁, 右鍵拖拽 |
Shell擴展操做全部選中的文件。它們的名字以相同於拷貝到剪裁板的格式傳遞 |
經IPersistFile初始化 |
左鍵拖拽,圖標 |
Shell擴展在文件上操做,不管其是否被選中,名字以Unicode串形式傳遞 |
啓動Shell擴展的過程由調用一個或多個初始化接口的方法組成。當探測器感受到它可能要觸發Shell擴展的事件時,它知道註冊了哪種擴展,以及怎樣初始化它。它所要作的所有工做就是附加對適當接口的查詢操做。
咱們的目的是要詳細描述當Shell擴展須要時IShellExtInit和IPersistFile接口的工做過程,所以,如今讓咱們看一下喚醒屬性頁Shell擴展時IShellExtInit接口的工做過程(咱們也將在IconHandler擴展中討論IPersistFile的初始化過程)。
IShellExtInit接口
咱們這裏所涉及到的屬性頁擴展是經過IShellExtInit接口的方式裝入的,它只有一個方法稱爲Initialize(),探測器喚醒並傳遞三個參數:
類型 |
參數 |
描述 |
LPCITEMIDLIST |
pidlFolder |
對於屬性頁擴展老是NULL |
LPDATAOBJECT |
Lpdobj |
指向IDataObject對象的指針,能夠用這個對象得到當前選中的文件 |
HKEY |
hkeyProgID |
所涉及文件的註冊表鍵 |
由於同一個接口服務於幾種類型的擴展,頭一個和第三個參數能夠有不一樣的意義,這依賴於被初始化的類型。對於屬性頁,不涉及到文件夾,因此pidlFolder變量沒有使用。hkeyProdID參數是HKEY Handle,指向註冊表鍵,包含對象要喚醒的文件信息。例如,若是Shell擴展操做WMF文件,考慮上一章的例子,則hkeyProdID將握有:
HKEY_CLASSES_ROOT
/WinMetafile
對於屬性頁的擴展最重要的變量是lpdobj,它包含了指向實現IDataObject接口對象的指針。這是一個已知的接口,有許多用戶接口都使用這個接口。基本上,IDataObject定義了運行模塊之間要交換的數據塊的行爲,所以剪裁板和拖拽操做是它的主要應用領域。
拷貝數據到剪裁板和從剪裁板取得數據這種OLE方法說明了存儲和恢復指向實現IDataObject對象指針的狀況。一樣,當你使用COM接口拖拽數據時,源和目的數據交換也是經過IDataObject完成的。另外一個觀察IDataObject對象的方法是:把IDataObject對象做爲Windows Handle的演化——即,表示包含數據的內存塊的通用對象。這種加強提供了對數據的存儲能力:
具備精確格式的數據,不僅是通用的‘某些東西的指針’
在存儲介質中而不是在內存中的數據
同時容納更多的數據塊
IDataObject接口輸出方法來取得和枚舉數據。特別,它使用象FORMATETC和STGMEDIUM這樣的結構來定義格式和數據存儲介質。在得到IDataObject指針後,你能夠詢問它以便發現它是否在必定介質上包含特定格式的數據。過一會,在咱們揭示了它怎樣應用於屬性頁擴展以後,這一點就更清楚了。
回到屬性頁的Shell擴展。此時,傳遞給Initialize()的IDataObject對象包含一個HDROP Handle。在第6章咱們看到,這個Handle包含了一個文件名列表,咱們可使用象DragQueryFile()這樣的函數遍歷這個列表。對於屬性頁擴展,這個列表包含在Shell中全部當前選中文件的名字。
屬性頁對話框僅在從Shell右擊一個或多個選中文件而且從導出的關聯菜單中選擇屬性項後彈出。選中的文件列表經由實現IDataObject的對象傳遞給Shell擴展,並且包含了CF_HDROP格式的數據。CF_HDROP是標準剪裁板格式之一,這種形式的數據存儲在稱之爲HDROP的全程內存Handle上。
STGMEDIUM medium;
HDROP hDrop;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(SUCCEEDED(hr))
hDrop = static_cast<HDROP>(medium.hGlobal);
上面代碼段說明怎樣從IDataObject指針恢復HDROP Handle。GetData()經過FORMATETC變量接收要恢復的數據描述,若是成功,則經由STGMEDIUM變量返回。FORMATETC結構定義以下:
typedef struct tagFORMATETC
{ CLIPFORMAT cfFormat;
DVTARGETDEVICE* ptd;
DWORD dwAspect;
LONG lindex;
DWORD tymed;
} FORMATETC, *LPFORMATETC;
就咱們的觀點,值得注意的成員是cfFormat和tymed,它們分別說明數據格式和存儲介質類型。於是代碼中CF_HDROP是數據格式,而TYMED_HGLOBAL表示全程內存Handle做爲數據返回的存儲介質。其它可能的存儲介質是磁盤文件,原文件和指向IStorage或IStream對象的指針。
下面咱們給出實現‘Do_nothing’的ATL類,其函數在創建示例工程(project)時將重載,下面清單是IShellExtInitImpl.h頭文件,它包含大多數IShellExtInit接口的基本實現。
// IShellExtInitImpl.h
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IShellExtInitImpl : public IShellExtInit
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellExtInitImpl)
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY)
{
return S_FALSE;
}
};
IShellPropSheetExt接口
提供添加新屬性頁方法的接口是IShellPropSheetExt,它輸出兩個函數(在IUnknown之上的函數):AddPages()和ReplacePage()。第一個函數有下面形式的參數:
類型 |
參數 |
描述 |
LPFNADDPROPSHEETPAGE |
lpfnAddPage |
指向實際添加頁面函數的指針 |
LPARAM |
lParam |
必須傳遞給由lpfnAddPage指定的函數的變量 |
AddPages()創建新的屬性頁,並調用從lpfnAddPage參數接收的函數。這是一個由Shell定義的回調函數,它有下面的原型:
BOOL CALLBACK AddPropSheetPageProc(HPROPSHEETPAGE hpage, LPARAM lParam);
第二個變量老是由Shell傳遞來,使第一個參數得到AddPages()的任務。對每個註冊屬性頁的Shell擴展,這個回調函數都被調用一次,特別是Shell正在顯示屬性對話框時。AddPages()函數能夠添加一個或多個頁面,然而,在加多個頁面時,它必須創建頁面並重復調用由lpfnAddPage指向的函數。
另外一個由IShellPropSheetExt輸出的方法,ReplacePage(),僅僅用於置換控制面板小程序的屬性頁在咱們的示例中沒有實現這個函數,但它的原型是:
HRESULT ReplacePage(UINT uPageID, // 要置換的頁索引
LPFNADDPROPSHEETPAGE lpfnReplacePage, // 指向置換頁函數的指針
LPARAM lParam); // 附加到函數的變量
遵照咱們早期的承諾,下面的清單是IShellPropSheetExtImpl.h,包含了IShellPropSheetExt接口的基本實現:
// IShellPropSheetExtImpl.h
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IShellPropSheetExtImpl : public IShellPropSheetExt
{
public:
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellPropSheetExtImpl)
// IShellPropSheetExt
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM)
{
return S_FALSE;
}
STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM)
{
return E_NOTIMPL;
}
};
添加新的屬性頁
爲了適當地開始一個工程(project),咱們創建一個新的ATL DLL工程(project)WMFProp,並添加一個簡單的對象PropPage。在ATL 部件框架生成之後,咱們須要對新對象的頭文件作一些改變,PropPage.h:
// PropPage.h : 聲明 CPropPage 對象類
#ifndef __PROPPAGE_H_
#define __PROPPAGE_H_
#include "resource.h" // 主程序符號
#include <comdef.h> // 標準接口 GUIDs
#include "IShellExtInitImpl.h" // IShellExtInit
#include "IShellPropSheetExtImpl.h" // IShellPropSheetExt
BOOL CALLBACK PropPage_DlgProc(HWND, UINT, WPARAM, LPARAM);
////////////////////////////////////////////////////////////////////////////
// CPropPage
class ATL_NO_VTABLE CPropPage :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CPropPage, &CLSID_PropPage>,
public IShellExtInitImpl,
public IShellPropSheetExtImpl,
public IDispatchImpl<IPropPage, &IID_IPropPage, &LIBID_WMFPROPLib>
{
public:
CPropPage()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_PROPPAGE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CPropPage)
COM_INTERFACE_ENTRY(IPropPage)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IShellPropSheetExt)
END_COM_MAP()
// IPropPage
public:
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM);
};
#endif //__PROPPAGE_H_
須要實現的接口方法是Initialize()和AddPages()。咱們還聲明瞭靜態成員函數PropPage_DlgProc(),它用於定義被添加頁面的行爲——這是新頁面的窗口過程。
Initialize()函數的代碼
Initialize()方法代碼以下:
HRESULT CPropPage::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT
lpdobj, HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 初始化通用控件(屬性頁是通用控件)
InitCommonControls();
// 從IDataObject得到選中文件名,數據以CF_HDROP格式存儲
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
HDROP hDrop = static_cast<HDROP>(medium.hGlobal);
if(DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0) == 1)
{
DragQueryFile(hDrop, 0, m_szFile, sizeof(m_szFile));
hr = NOERROR;
}else
hr = E_INVALIDARG;
ReleaseStgMedium(&medium);
return hr;
}
因爲屬性頁是通用控件,咱們須要初始化適當的庫。這也說明必須#include commctrl.h,和引入comctl32.lib庫。在使用前面描述的技術得到選中文件後,檢查有多少選中文件。爲簡單起見,若是有多個選中文件,咱們退出這個函數,這就是下面代碼所作的操做:
if(DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0) == 1)
{
...
}
如上調用DragQueryFile()以後,返回選中文件數量。下一行則抽取第一個也是惟一一個文件(它的索引爲0),並把它的名字存入m_szFile緩衝:
DragQueryFile(hDrop, 0, m_szFile, sizeof(m_szFile));
最後,全部活動完成後,經過調用ReleaseStgMedium()釋放存儲介質結構。
AddPages()函數的代碼
AddPages()函數的代碼以下:
HRESULT CPropPage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
lstrcpy(g_szFile, m_szFile);
// 創建新頁面須要填充PROPSHEETPAGE 結構
PROPSHEETPAGE psp;
ZeroMemory(&psp, sizeof(PROPSHEETPAGE));
psp.dwSize = sizeof(PROPSHEETPAGE);
psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT;
psp.hInstance = _Module.GetModuleInstance();
psp.pszTemplate = MAKEINTRESOURCE(IDD_WMFPROP);
psp.pszTitle = __TEXT("預覽");
psp.pfnDlgProc = PropPage_DlgProc;
psp.lParam = reinterpret_cast<LPARAM>(g_szFile); // 爲dlgproc定製數據
psp.pcRefParent = reinterpret_cast<UINT*>(&_Module.m_nLockCnt);
// 創建新頁面
HPROPSHEETPAGE hPage = ::CreatePropertySheetPage(&psp);
// 添加頁面到屬性頁
if(hPage != NULL)
{
if(!lpfnAddPage(hPage, lParam))
::DestroyPropertySheetPage(hPage);
return NOERROR;
}
return E_INVALIDARG;
}
新頁面包含一個對話框,既沒有標題也沒有邊框,並且在上面代碼中,PROPSHEETPAGE結構的pszTemplate成員被設置爲它的ID。咱們設計的對話框包含單個圖像控件,具備SS_ENHMETAFILE風格,取名爲IDC_METAFILE,附加一個對話框模板到工程的資源中對屬性頁面的Shell擴展老是必要的。然而,對話框要求對話框過程處理全部它包含的控件。在上例中是PropPage_DlgProc()簡單地響應WM_INITDIALOG和繪製原文件,爲此,咱們使用在前一章中定義的函數。因爲對話框過程不能訪問類成員,咱們經過PROPSHEETPAGE結構的lParam字段傳遞要顯示的文件名,而且對話框過程接收指向這個結構的指針做爲WM_INITDIALOG消息的lParam變量。
BOOL CALLBACK PropPage_DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
HWND hwndMeta = GetDlgItem(hwnd, IDC_METAFILE);
LPPROPSHEETPAGE lppsp = reinterpret_cast<LPPROPSHEETPAGE>(lParam);
DisplayMetaFile(hwndMeta, reinterpret_cast<LPTSTR>(lppsp->lParam));
return FALSE;
}
return FALSE;
}
註冊Shell擴展
咱們前面說過,若是沒有正確地註冊Shell擴展,它們將不能工做:探測器不能找到要加載的模塊。每個Shell擴展每次關聯到指定的文件對象是經過文件類型(好比說EMF),或通用的對象(如文件夾)。於是,在註冊Shell擴展時,你必須考慮是否增長安裝文件類型的信息。若是你寫的Shell擴展是對系統文件類型的好比BMP,TXT,文件夾或 *,就沒必要註冊新文件類型了。然而對於客戶的文件類型(好比說XYZ),或沒有默認定義的文件類型(就象EMF和WMF),你應該保證註冊信息的輸入。假定文件類型的註冊信息正確地註冊了,咱們仍然須要添加幾行到由ATL應用大師產生的標準註冊腳本中。這些行應該與Shell擴展操做的文件類型或一同工做的文件類型相關。此時Shell擴展不只必須註冊鏈接WMF和EMF,還要在下面這些鍵下注冊:
HKEY_CLASSES_ROOT
/WinMetafile
對應WMFs, 和
HKEY_CLASSES_ROOT
/EnhMetafile
對應EMFs。
Shell擴展必須在指定文件類鍵的shellex子鍵下注冊,在shellex下,你須要創建附加的鍵分組各類類型的擴展,並且這些都有特定的名字。註冊屬性頁Shell擴展的鍵爲PropertySheetHandlers,在其下能夠列出對這個文件類全部屬性頁Shell擴展的CLSID。
有點陌生的是Shell擴展類型容許定義同一個文件類的多個服務器,它們被順序調用。例如,極可能是有三個COM服務器實現位圖文件類型的三個關聯菜單的不一樣擴展。對於全部Shell擴展,除了那些處理剪裁板和左鍵拖拽的擴展,都容許有多重擴展存在。後面咱們還要討論這個問題。
下面清單說明怎樣將默認的註冊腳本改變爲正確註冊屬性頁Shell擴展的腳本。
HKCR
{
WMFProp.PropPage.1 = s 'PropPage Class'
{
CLSID = s '{0D0E3558-8011-11D2-8CDB-505850C10000}'
}
WMFProp.PropPage = s 'PropPage Class'
{
CLSID = s '{0D0E3558-8011-11D2-8CDB-505850C10000}'
CurVer = s 'WMFProp.PropPage.1'
}
NoRemove CLSID
{
ForceRemove {0D0E3558-8011-11D2-8CDB-505850C10000} = s 'PropPage Class'
{
ProgID = s 'WMFProp.PropPage.1'
VersionIndependentProgID = s 'WMFProp.PropPage'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{0D0E354B-8011-11D2-8CDB-505850C10000}'
}
}
WinMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB-505850C10000}
}
}
}
EnhMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB-505850C10000}
}
}
}
}
下圖說明了註冊加強元文件後的註冊表狀態。注意,其中有三個屬性頁的Shell擴展。若是你還有另外一個加強元文件的Shell擴展——例如管理關聯菜單——它們應該以一樣的方法註冊,可是是在另外一個子鍵下。定位在與PropertySheetHandlers同層。
如今Shell擴展正確地註冊了之後,你就能右擊EMF或WMF文件,而且有下面的行爲出現:
測試Shell擴展
到目前爲止咱們已經編寫並註冊了一個Shell擴展,如今咱們來看一下它是否作了它應該作的工做。運行Shell擴展的惟一方法是啓動探測器並執行引發Shell擴展動做的活動,可是要使探測器確信你的擴展存在多是比較困難的。在必定場合下,你可能須要註銷登陸,甚至重啓機器來使Shell加載更新版的擴展,相反,對比重啓機器,簡單地關閉探測器可能更好一點,並且可使用任務條實用程序,咱們在第9章中就是這麼作的。還有就是按F5鍵,但這種方法不能總奏效。
參見這一章後面的Shell擴展開發者手冊,其中有更詳細的討論
除了這些小困難以外,咱們如今假設正在運行你的擴展。當你感受到一個錯誤,而且須要排除代碼找到錯誤發生點時複雜的事情發生了。排除Shell擴展的錯誤不是直覺的任務,咱們須要仔細地檢查擴展操做的過程。第一步是設置explorer.exe爲排錯會話的可執行程序。由於Shell擴展是DLL,而且不是獨立可執行程序,所以這一步是必要的。注意,你須要指定探測器的全路徑:
第二步是要保證你的Shell擴展工程在VC++IDE中打開。這個技巧是中止Shell,而後在排錯器下導出它的新實例運行,這比想象的要困難一點。若是你簡單地運行排錯器,能夠引發探測器窗口的出現,可是這並非說新的Shell進程已經啓動,對於要發生的排錯,你首先須要終止Shell進程,而不終止機器上的其它進程,而後再次運行排錯器,它將實際地創建一個可排錯的Shell進程。
要中止Shell,你能夠編程發送WM_QUIT消息到惟一的窗口類‘program’(咱們在第9章中已經討論了這個技術),要手動作這個工做,執行下面的操做:
從開始菜單中選擇‘關閉’,而且在按下Ctrl-Alt-Shift時點擊‘取消’。這並不容易作到,可是它能工做。當你這樣作了以後,任務條消失,你將感受到系統重啓了,可是並無致使機器的重啓。沒有任何錯誤發生,全部都在控制之中。
使用Alt-Tab鍵導出VC++窗口到頂部,而後運行排錯器,如今任務條將再次出現,它標誌着新的Shell進程在VC++的排錯器下運行。
如今所要作的是與任何其它程序排錯同樣:點擊‘Build | 啓動排錯 | Go’菜單項。當探測器窗口顯示出來時,執行導出Shell擴展的活動。在這個例子中你應該選擇WMF文件,右擊,並打開屬性對話框。
你放置在代碼中的斷點如今能象一般同樣被感受到,而且在遇到時引發過程中止。在完成排錯以後雙擊桌面將導出任務管理器窗口來到前面:
選擇‘文件 | 運行’,導出探測器,全部事情都恢復到之前的狀態。咱們給出的並非你天天都要操做的過程,可是它倒是可以解決Shell擴展排錯的問題。
值得注意的是控制檯小程序——它們老是包含一系列賬單頁面——不是運行在探測器地址空間中的。也就是說你不能使用上面描述的技術對它們排錯。相反,應該指定運行rundll32.exe做爲排錯會話的可執行程序。
在Windows NT下排錯
若是須要在NT下測試,咱們建議在下面的註冊鍵上添加你本身的值:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
添加的值稱爲DesktopProcess,其類型爲REG_DWORD,值爲1。設置了這個值以後,從新登陸,你將發現WindowsNT的Shell被劃分紅兩個部分——桌面,任務條和托盤域運行在文件夾和文件的不一樣進程中。如今在VC++ 環境下運行探測器,你實際正在啓動能夠排錯的新進程,並且任何衝突都不影響穩定的系統桌面。
卸載Shell擴展
另外一個關於Shell擴展測試的科目是肯定何時卸載Shell擴展。與其它COM對象同樣,Shell擴展是持續流目標,要求經過DllCanUnloadNow()導出卸載過程。模塊是否能夠被卸載依賴於它內部的引用計數。沒有自動機理來從內存刪除引用計數已經變爲0的模塊,所以探測器調用DllCanUnloadNow()越快,無用的Shell擴展卸載的就越快。注意,卸載後的Shell擴展模塊是能夠安全再編譯的,這對於Shell擴展在開發期間是十分重要的。
默認狀況下,探測器每十秒鐘嘗試一次卸載Shell擴展。資料說明能夠經過設置下面註冊鍵的默認值爲1來改變這個卸載嘗試的頻率:
HKEY_LOCAL_MACHINE
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/AlwaysUnloadDll
設置這個鍵在較老的系統上並無多大改變——Shell擴展的卸載沒有更快。
再說屬性頁的Shell擴展
上面的例子僅在選種單個文件時才能工做,並且沒有阻止咱們爲每一個被選中文件添加屬性頁,例如:
這種改變要求的代碼不是主要的,甚至能夠用同時運行多個擴展來實現這個目的——探測器將順序管理它們。惟一的缺點是你可能須要附加某些屬性頁的拷貝。下面就看一下咱們須要作哪些改變。
修改代碼來支持多重選擇
要作的頭一件也是最顯然的一件事就是Shell擴展的類聲明,以使其反映出咱們再也不使用單文件保持軌跡,而是使用列表文件名。這個列表有一個上限,由於prsht.h(屬性頁頭文件)限制其任何一個頁表上的頁數最大到100,助記常量爲MAXPROPPAGES。
這說明在一個頁表控件上不可能管理超過100的頁面數——咱們已經注意到這個控件不能有超過六行的頁面,所以合理的最大數是30—35頁。下面是咱們的新版本IShellPropSheetExt.h:
// IShellPropSheetExtImpl.h (多選版本)
//
//////////////////////////////////////////////////////////////////////
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IShellPropSheetExtImpl : public IShellPropSheetExt
{
public:
TCHAR m_aFiles[MAXPROPPAGES][MAX_PATH];
int m_iNumOfFiles;
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellPropSheetExtImpl)
// IShellPropSheetExt
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM)
{
return S_FALSE;
}
STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM)
{
return E_NOTIMPL;
}
};
在Initialize()和AddPages()的實現中代碼也要作稍微的改變。下面是新的Initialize():
HRESULT CPropPage::Initialize(LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj, HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 初始化通用控件
InitCommonControls();
// 獲取CF_HDROP格式數據
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
HDROP hDrop = static_cast<HDROP>(medium.hGlobal);
// 取得選中文件數
m_iNumOfFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
// 規格化到容許的最大數
m_iNumOfFiles = (m_iNumOfFiles >= MAXPROPPAGES ? MAXPROPPAGES : m_iNumOfFiles);
// 抽取和管理全部選中的文件
for(int i = 0 ; i < m_iNumOfFiles ; i++)
DragQueryFile(hDrop, i, m_aFiles[i], MAX_PATH);
Rele〉 0aseStgMedium(&medium);
return hr;
}
如今全部文件都存儲在文件名數組中了。它們將在AddPages()中一次處理:
HRESULT CPropPage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
for(int i = 0 ; i < m_iNumOfFiles ; i++)
{
// 檢查選中的文件是否爲元文件
LPTSTR p = PathFindExtension(m_aFiles[i]);
if(lstrcmpi(p, __TEXT(".WMF")) && lstrcmpi(p, __TEXT(".EMF")))
continue;
// 分配要傳遞的串。它將在 dlgproc 中被釋放。
LPTSTR psz = new TCHAR[MAX_PATH];
lstrcpy(psz, m_aFiles[i]);
// 剝離路徑和擴展名,以顯示在標題上
LPTSTR pszTitle = PathFindFileName(m_aFiles[i]);
PathRemoveExtension(pszTitle);
// 填寫PROPSHEETPAGE結構
PROPSHEETPAGE psp;
ZeroMemory(&psp, sizeof(PROPSHEETPAGE));
psp.dwSize = sizeof(PROPSHEETPAGE);
psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT;
psp.hInstance = _Module.GetModuleInstance();
psp.pszTemplate = MAKEINTRESOURCE(IDD_WMFPROP);
psp.pszTitle = pszTitle;
psp.pfnDlgProc = PropPage_DlgProc;
psp.lParam = reinterpret_cast<LPARAM>(psz);
psp.pcRefParent = reinterpret_cast<UINT*>(&_Module.m_nLockCnt);
HPROPSHEETPAGE hPage = ::CreatePropertySheetPage(&psp);
// 添加頁面到屬性頁上
if(hPage != NULL)
if(!lpfnAddPage(hPage, lParam))
:: DestroyPropertySheetPage(hPage);
}
return NOERROR;
}
關於這個版本的AddPages()函數,有幾點須要注意,首先,咱們設置屬性頁的標題爲沒有路徑和擴展名的文件名,這使用了一些來自shlwapi.dll的函數,所以#include <shlwapi.h> 和鏈接shlwapi.lib是必須的。第二,在清單中註釋了引用要在對話框過程當中刪除的指針,這個指針是在循環中分配的,因此如今的PropPage_DlgProc()應該是:
BOOL CALLBACK PropPage_DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
HWND hwndMeta = GetDlgItem(hwnd, IDC_METAFILE);
LPPROPSHEETPAGE lppsp = reinterpret_cast<LPPROPSHEETPAGE>(lParam);
DisplayMetaFile(hwndMeta, reinterpret_cast<LPTSTR>(lppsp->lParam));
delete [] reinterpret_cast<LPTSTR>(lppsp->lParam);
return FALSE;
}
return FALSE;
}
最後,函數如今可以識別WMF/EMF與其它文件類型——它接收前者而拒絕後者。當選中了必定數量的文件時,你不用保證它們都是一樣類型的。這就是說在右擊給定的屬性對話框時,你並不須要選擇指望類型的文件,所以也不能保證你的擴展被使用。例如,選擇EMF和BMP文件,在選中的BMP上右擊請求屬性對話框時,你將得到BMP的對話框,相反,若是你的文件是元文件或在元文件上右擊,你所得到的是下面的情形:
關聯菜單
關於添加新項到關聯菜單,Shell擴展是最靈活的技術,由於它們給出了事件的所有控制。在前一章中,咱們探討了使用註冊表操做來達到一樣目的的方法,可是那種技術引發外部代碼段執行。用Shell擴展,你能夠運行直接與Shell通信的代碼段,接收和返回信息。若是你編寫和註冊了關聯菜單的Shell擴展,你能夠有機會選擇指定菜單項串,狀態條描述和每次菜單被顯示的行爲。只要你喜歡,老是能編程改變它們,而不須要修改任何註冊表。
實現IContextMenu接口
處理關聯菜單的Shell擴展就是編寫一個實現IContextMenu接口的COM服務器。除了這個變化外,在咱們前面描述的示例中不須要作任何改動。從IUnknown導出的IContextMenu有三個函數:
GetCommandString()
InvokeCommand()
QueryContextMenu()
它們分別恢復菜單項的描述,響應點擊操做和添加新命令到菜單。
新項的幫助文字
GetCommandString()有下面的原型:
HRESULT GetCommandString(UINT idCmd, // 須要描述的菜單命令ID
UINT uFlags, // 指定要作什麼的標誌
UINT* pwReserved, // 保留,老是NULL
LPSTR pszName, // 接收要恢復串的緩衝(最大 40)
UINT cchMax); //接收串的實際長度
GetCommandString()函數的uFlags可用的取值是:
標誌 |
描述 |
GCS_HELPTEXT |
Shell要求項的描述串 |
GCS_VALIDATE |
Shell簡單地想要知道是否具備這個ID的項存在和有效 |
GCS_VERB |
Shell要求這個菜單項動詞的語言無關的名 |
動詞是實施命令的名字(咱們在前面章節中已經解釋過了,特別在第8章)。動詞可經過ShellExecute()和ShellExecuteEx()函數執行。在經過註冊表靜態添加新的菜單項時,創建的鍵名就是語言無關的動詞,其後的命令則隱藏在‘Command’子鍵下。在動態添加菜單項時,你應該實現InvokeCommand()來提供相似‘Command’鍵的行爲,而且適當地響應GCS_VERB標誌令Shell知道新命令的動詞。
注意,你傳遞的任何幫助文字都將在40字符以後返回,儘管傳遞了較長的串,也不要截斷除了串自己以外的任何東西。
新項的行爲
InvokeCommand()是在用戶點擊關聯菜單項時被調用的方法。其原型爲:
HRESULT InvokeCommand(LPCMINVOKECOMMANDINFO lpici);
CMINVOKECOMMANDINFO結構聲明以下:
typedef struct _CMINVOKECOMMANDINFO
{
DWORD cbSize;
DWORD fMask;
HWND hwnd;
LPCSTR lpVerb;
LPCSTR lpParameters;
LPCSTR lpDirectory;
INT nShow;
DWORD dwHotKey;
HANDLE hIcon;
} CMINVOKECOMMANDINFO, *LPCMINVOKECOMMANDINFO;
讓咱們更詳細地討論這個結構:
成員 |
描述 |
cbSize |
這個結構的尺寸 |
fMask |
容許dwHotkey和hIcon成員,和防止任何UI活動的屏蔽位,就象消息框的標誌同樣。 |
hwnd |
菜單的父窗口 |
lpVerb |
一個命令ID給出的DWORD類型值(高字爲0),或表示要執行動詞的串 |
lpParameters |
若是接口從Shell調用,老是NULL |
lpDirectory |
若是接口從Shell調用,老是NULL |
nShow |
若是啓動新應用,這是一個傳遞給ShowWindow()的 SW_ 型常量。 |
dwHotKey |
由命令分配給應用啓動的熱鍵。若是fMask關閉了它的特定位,這個熱鍵沒必要考慮。 |
hIcon. |
由命令分配給啓動應用的圖標,若是fMask關閉了它的特定位,這個圖標沒必要考慮。 |
fMask的合法值以下:
值 |
描述 |
CMIC_MASK_HOTKEY |
dwHotKey成員是可用的 |
CMIC_MASK_ICON |
hIcon成員是可用的 |
CMIC_MASK_FLAG_NO_UI |
沒有能夠影響用戶界面的活動發生(例如,創建窗口或消息框) |
lpVerb成員是一個32位值,有兩種方法肯定其內容,它能夠是調用
lpVerb = MAKEINTRESOURCE(idCmd, 0);
的結果。這裏idCmd是菜單項的ID,而lpVerb也能夠表示要執行動詞的名字。此時,高字不爲0,這個值實際指向一個串。
與其它Shell相關的接口相似,IContextMenu也能夠從Shell以外調用,不用響應在Shell元素上的UI活動。例如,當你得到了IShellFolder指針後,就能夠請求綁定在這個文件夾或文件對象上的IContextMenu接口。而後就可使用IContextMenu編程喚醒動詞,而不須要經過Shell。此時的lpParameters和lpDirectory可能不是NULL。
此外,你還可使用ShellExecuteEx()來調用Shell擴展動態添加的動詞。此時能夠經過這個接口函數指定附加的參數和工做目錄,這就是最終所填寫的lpParameters和lpDirectory變量。(參見第8章)
添加新項
在創建給定文件對象的關聯菜單時,Shell經過調用QueryContextMenu()查詢全部註冊的關聯菜單Shell擴展來添加擴展所擁有的項。這個函數的原型是:
HRESULT QueryContextMenu(HMENU hmenu, // 要添加項的菜單Handle
UINT indexMenu, // 被添加的第一項的索引(從0開始)
UINT idCmdFirst, // 新項的最低可用命令ID
UINT idCmdLast, // 新項的最高可用命令ID
UINT uFlags); // 影響關聯菜單的屬性
在添加新菜單項時,Shell指示頭一個添加項的位置,以及命令ID的取值範圍。下面一小段代碼顯示了典型的經過QueryContextMenu()插入新項的方法:
idCmd = idCmdFirst;
lstrcpy(szItem, ...);
InsertMenu(hMenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
在全部uFlags變量可用的標誌中,咱們所困擾的是CMF_NORMAL和CMF_DEFAULTONLY。其它的對於‘簡單’的Shell擴展是沒有意義的,而主要是應用於命名空間擴展。下面是這些值的完整列表:
標誌 |
描述 |
CMF_CANRENAME |
若是設置,命名空間擴展應該添加一個‘重命名’項 |
CMF_DEFAULTONLY |
用戶雙擊,命名空間擴展能夠添加它爲默認項。Shell擴展不該該作任何事情,事實上若是這個標誌設置,應該避免添加項。 |
CMF_EXPLORE |
當探測器打開樹窗口時設置此標誌 |
CMF_INCLUDESTATIC |
Shell擴展不顧此標誌 |
CMF_NODEFAULT |
菜單不該該有默認項,Shell擴展忽略這個標誌,但命名空間擴展應該避免定義默認項 |
CMF_NORMAL |
非特殊狀況,Shell擴展能夠添加它們的項。 |
CMF_NOVERBS |
Shell擴展忽略這個標誌。它用於‘發送到’菜單。 |
CMF_VERBSONLY |
Shell擴展忽略這個標誌。它用於快捷方式對象的菜單 |
你確定很奇怪Shell擴展爲何忽略在命名空間擴展中有用的標誌,或忽略應用於特定菜單如‘發送到’和快捷方式菜單的標誌。IContextMenu不是一個Shell擴展接口嗎?
實際上,答案是否認的,IContextMenu是提供關聯菜單功能的通用COM接口。幾乎全部的系統菜單均可以經過在註冊表的適當位置註冊關聯菜單處理器來擴展——Shell加載它,於是提供添加和管理客戶菜單項的可能性。IContextMenu可用於在探測器窗口之外工做,咱們在後面將給出這方面的例子。命名空間擴展是一個定製的Shell觀察,能夠直接調用提供的關聯菜單到用戶,所以IContextMenu也影響命名空間擴展。
QueryContextMenu()的返回值
與其它COM 函數同樣,QueryContextMenu()返回HRESULT值。在不少狀況下,你可使用預約義常量,偶爾,須要格式化特定的返回值。QueryContextMenu()就是須要這樣作的函數之一。咱們都知道HRESULT是32位值,其位被分紅三部分:嚴格(severity),簡易(facility)和代碼(code)。QueryContextMenu()要求你返回代碼到特定值,和0。特別是,你應該返回添加的菜單項數。要格式化HRESULT,MAKE_HRESULT()宏是極爲有用的:
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdList);
可執行程序的相關列表
如今咱們把學過的關於關聯菜單的全部技術都串聯在一塊兒作一個練習。在操做探測器時你可能會遇到成百上千的可執行程序,是否有人能告訴你這些程序引用了什麼庫呢?程序有一個靜態引用的模塊列表,它稱之爲相關列表。
經過掃描Win32 可執行程序的二進制格式(假設對Win32簡攜可執行格式有很好的理解),就有可能抽取出一個應用所須要的全部DLL名。在這個例子中,咱們打算實現一個工具,做爲關聯菜單對EXE和DLL文件查看它們的相關列表。
在開始以前,咱們要說明幾件事,首先,這個工具不須要運行應用——這將限制對其檢查字節。其次,它僅能恢復那些在代碼中顯式引入的DLL。這是由於僅靜態鏈接到工程中的DLL在代碼中留有標記,若是程序經過LoadLibrary()動態裝入DLL,這個DLL不在引入表中引用,咱們就不能跟蹤它。
創建關聯菜單的擴展
咱們並不打算就獲取Win32可執行程序相關列表給出方方面面的細節說明,由於這是一個十分複雜的科目而且超出了本書的範圍。若是你感興趣,請參考相關的MSDN資料。在這個例子中,咱們使用相對新的DLL,其名字爲ImageHlp。這個庫並不輸出特殊的函數來得到文件名,而是經過使用其中的一個例程,來完成這些操做。
開始,使用ATL COM應用大師創建DLL工程(project),取名爲Depends,加入一個新的簡單對象ExeMenu,接受全部默認的選項。這是一個實現關聯菜單Shell擴展所要求接口的對象:IContextMenu和IShellExtInit。下面是咱們須要對ExeMenu.h主頭文件所做的改變:
#include "resource.h" // 主符號
#include "IContextMenuImpl.h" // IContextMenu
#include "IShellExtInitImpl.h" // IShellExtInit
#include "DepListView.h" // 對話框
#include <comdef.h> // 接口 IDs
//////////////////////////////////////////////////////////////////////////
// CCExeMenu
class ATL_NO_VTABLE CExeMenu :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CExeMenu, &CLSID_CExeMenu>,
public IShellExtInitImpl,
public IContextMenuImpl,
public IDispatchImpl<IExeMenu, &IID_IExeMenu, &LIBID_DEPENDSLib>
{
public:
CExeMenu()
{
}
TCHAR m_szFile[MAX_PATH]; // 可執行文件名
CDepListView m_Dlg; // 顯示結果的對話框
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT , UINT, UINT);
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
DECLARE_REGISTRY_RESOURCEID(IDR_EXEMENU)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CExeMenu)
COM_INTERFACE_ENTRY(IExeMenu)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
// IExeMenu
public:
};
CExeMenu類從IShellExtInitImpl和IContextMenuImpl兩個ATL類中導出,它提供IShellExtInit 和 IContextMenu接口的基本實現。IShellExtInitImpl.h頭文件與咱們在前一個例子中使用的同樣,而IContextMenuImpl.h頭文件有以下形式:
// IContextMenuImpl.h
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IContextMenuImpl : public IContextMenu
{
public:
// 數據
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IContextMenuImpl)
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT)
{
return S_FALSE;
}
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO)
{
return S_FALSE;
}
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT , UINT, UINT)
{
return S_FALSE;
}
};
退一步說,這裏的是最小實現。在其它狀況下,你可能須要準備更有效的類,並加強代碼可重用的質量,然而,對於咱們的例子,這段代碼足夠了。剩下的就是要提供兩個接口所有函數的代碼,它們都包含在ExeMenu.cpp中:
// QueryContextMenu
HRESULT CExeMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
UINT idCmdLast, UINT uFlags)
{
// 這個Shell擴展打算在EXE文件的關聯菜單上提供相關列表
UINT idCmd = idCmdFirst;
// 添加新菜單項
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION,idCmd++,
__TEXT("Dependency &List"));
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdFirst);
}
// InvokeCommand
HRESULT CExeMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
// 創建模式對話框顯示信息
lstrcpy(m_Dlg.m_szFile, m_szFile);
m_Dlg.DoModal();
return S_OK;
}
// 取得命令串
HRESULT CExeMenu::GetCommandString(UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszText, UINT cchMax)
{
// 咱們不關心命令ID,由於咱們只有單個項
if(uFlags & GCS_HELPTEXT)
lstrcpyn(pszText, __TEXT("顯示模塊須要的全部DLL"), cchMax);
return S_OK;
}
// Initialize
HRESULT CExeMenu::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 取得 CF_HDROP
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
// 取得選中文件名
DragQueryFile(reinterpret_cast<HDROP>(medium.hGlobal), 0, m_szFile, MAX_PATH);
ReleaseStgMedium(&medium);
return hr;
};
應該看到,Initialize()代碼與前面屬性頁例子中的初始化代碼基本一致。
初始化關聯菜單擴展
前面咱們說過,Initialize()的參數對不一樣類型的Shell擴展是不一樣的。對於關聯菜單擴展,pidlFolder變量是文件夾的PIDL,它包含選中的文件對象。這些文件對象由lpdobj經過IDataObject接口指向,IDataObject接口咱們在上一個例子中遇到過。hKeyProgID參數指定了選中文件對象的文件類,並且,若是選中了多個對象,它指向有焦點的一個。
獲取可執行的關聯鏈表
這個擴展的目的是當用戶點擊‘相關列表’菜單項時:
Shell將調用InvokeCommand()方法導出對話框。在這個截圖中注意狀態條中的顯示文字,這是咱們經過GetCommandString()函數提供的串。咱們使用ATL對象大師添加一個對話框命名爲DepListView,而且加入一個公共數據成員m_szFile來保存文件名:
enum {IDD = IDD_DEPLISTVIEW};
TCHAR m_szFile[MAX_PATH];
對話框的初始化在其OnInitDialog()方法中發生,這要求包含shlobj.h 和 windowsx.h到DepListView.h的頂部:
LRESULT CDepListView::OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
// 準備列表觀察,使用前面章中定義的函數
HWND hwndList = GetDlgItem(IDC_LIST);
LPTSTR pszCols[] = {__TEXT("Library"), reinterpret_cast<TCHAR*>(280),
__TEXT("Version"), reinterpret_cast<TCHAR*>(103)};
MakeReportView(hwndList, pszCols, 2);
// 使用省略號設置文件名,若是它太長的話
TCHAR szTemp[60] = {0};
PathCompactPathEx(szTemp, m_szFile, 60, '//');
SetDlgItemText(IDC_FILENAME, szTemp);
// 得到引入表的尺寸
int iNumOfBytes = GetImportTableSize(m_szFile);
if(iNumOfBytes <= 0)
return 0;
// 取得COM分配器 並保留一些內存
LPMALLOC pM = NULL;
SHGetMalloc(&pM);
LPTSTR psz = static_cast<LPTSTR>(pM->Alloc(iNumOfBytes));
if(psz == NULL)
{
::MessageBox(0, __TEXT("沒有足夠的內存!"), 0, MB_ICONSTOP);
pM->Release();
return 0;
}
ZeroMemory(psz, iNumOfBytes);
// 訪問引入表
int iNumOfLibs = GetImportTable(m_szFile, psz);
if(iNumOfLibs <= 0)
{
pM->Release();
return 0;
}
int i = 0;
while(i < iNumOfLibs)
{
// p 爲列表觀察格式化NULL分割的串
TCHAR buf[2048] = {0};
LPTSTR p = buf;
lstrcpy(p, psz);
lstrcat(p, __TEXT("/0"));
p += lstrlen(p) + 1;
// 取得版本信息
TCHAR szInfo[30] = {0};
SHGetVersionOfFile(psz, szInfo, NULL, 0);
lstrcpy(p, szInfo);
lstrcat(p, __TEXT("/0"));
p += lstrlen(p) + 1;
// 添加傳到列表觀察
AddStringToReportView(hwndList, buf, 2);
// 下一個庫
psz += lstrlen(psz) + 1;
i++;
}
pM->Release();
return 1;
}
首先咱們經過添加兩個列來格式化報告列表觀察,一爲文件名,一爲版本號。第二,咱們讀出執行模塊的引入表,並格式化一個NULL分隔的串。爲了處理這個對話框,咱們重用了一些函數——MakeReportView()和AddStringToReportView(),以及SHGetVersionOfFile()函數。下圖顯示了最後的對話框:
這個對話框由表示爲IDC_LIST的報告列表和命名爲IDC_FILENAME的文字標籤組成。還要注意,咱們使用了shlwapi.dll中的PathCompactPathEx()函數來強迫文件名到固定的字符數——當文件名太長時自動插入省略號來截斷它。
咱們前面說過不打算深刻討論獲取相關列表的技術,可是這個過程有幾件事是須要提到的。ImageHlp API是在Windows9x和NT下可用的,它提供在可執行模塊產生的內存映像上操做的函數。還有些函數遍歷符號表把它映射進內存。(參見MSDN資料庫)。
特別值得注意的函數是BindImageEx(),它容許你獲取可執行模塊從外部庫引入的任何函數的虛地址。從咱們的觀點看,這個函數接受一個回調例程,而且傳遞每個它遇到的DLL名到這個例程。經過鉤住這些調用,咱們可以很容易地計算出須要多少字節來存儲整個名字列表(GetImportTableSize()),而且把全部名字都變成NULL分隔的串(GetImportTable())。
咱們打算用一個簡單的DLL來提供這些函數,頭文件爲DepList.h,應該在頂部包含#include DepListView.h:
#include <windows.h>
#include <imagehlp.h>
// 返回指定名DLL的字節數
int APIENTRY GetImportTableSize(LPCTSTR pszFileName);
// 用DLL名充填指定的緩衝
int APIENTRY GetImportTable(LPCTSTR pszFileName, LPTSTR pszBuf);
較大的源代碼是DepList.cpp:
#pragma comment(lib, "imagehlp.lib")
#include "DepList.h"
/*----------------------------------------------------------------*/
// GLOBAL 節
/*----------------------------------------------------------------*/
// 數據
LPTSTR* g_ppszBuf = NULL;
int g_iNumOfBytes = 0;
int g_iNumOfDLLs = 0;
// 回調
BOOL CALLBACK SizeOfDLLs(IMAGEHLP_STATUS_REASON, LPSTR, LPSTR, ULONG, ULONG);
BOOL CALLBACK GetDLLs(IMAGEHLP_STATUS_REASON, LPSTR, LPSTR, ULONG, ULONG);
/*----------------------------------------------------------------*/
// 過程:GetImportTableSize()
/*----------------------------------------------------------------*/
int APIENTRY GetImportTableSize(LPCTSTR pszFileName)
{
g_iNumOfBytes = 0;
// 綁定到可執行
BindImageEx(BIND_NO_BOUND_IMPORTS | BIND_NO_UPDATE,
const_cast<LPTSTR>(pszFileName), NULL, NULL, SizeOfDLLs);
return g_iNumOfBytes;
}
BindImageEx()的原型是:
BOOL BindImageEx(DWORD dwFlags,
LPSTR pszFileName,
LPSTR pszFilePath,
LPSTR pszSymbolPath,
PIMAGEHLP_STATUS_ROUTINE pfnStatusProc);
你必須在pszFileName中指定要操做的文件名,而且可能包含路徑。若是不包含路徑,可使用pszFilePath來指定搜索pszFileName的根路徑。更重要的是,這個函數回調pfnStatusProc中的例程,這個例程在函數綁定到指定可執行模塊期間被喚醒,下面是回調的原型:
BOOL CALLBACK BindStatusProc(IMAGEHLP_STATUS_REASON Reason,
LPSTR ImageName,
LPSTR DllName,
ULONG Va,
ULONG Parameter);
咱們惟一感興趣的參數是Reason 和 DllName。第二個參數的目的是顯然的,而第一個參數令你過濾對這個函數的衆多調用,使之專一於實際感興趣的。咱們僅想知道須要多少字節來存儲全部模塊的引用,以及它們是哪些模塊。SizeOfDLLs()是返回文件引入表尺寸的回調函數,GetDLLs()是經過調用BindImageEx()鏈接到全部綁定模塊名而得到返回NULL分隔串的函數。這個串與版本信息組合產生輸出顯示。
/*----------------------------------------------------------------*/
// 過程: GetImportTable
/*----------------------------------------------------------------*/
int APIENTRY GetImportTable(LPCTSTR pszFileName, LPTSTR pszBuf)
{
g_ppszBuf = &pszBuf;
g_iNumOfDLLs = 0;
// 綁定到可執行
BindImageEx(BIND_NO_BOUND_IMPORTS | BIND_NO_UPDATE,
const_cast<LPTSTR>(pszFileName), NULL, NULL, GetDLLs);
return g_iNumOfDLLs;
}
/*----------------------------------------------------------------*/
// 過程: SizeOfDLLs()
// Description.: 計算DLL尺寸的回調
/*----------------------------------------------------------------*/
BOOL CALLBACK SizeOfDLLs(IMAGEHLP_STATUS_REASON Reason,
LPSTR ImageName, LPSTR DllName, ULONG Va, ULONG Parameter)
{
if(Reason == BindImportModule || Reason == BindImportModuleFailed)
g_iNumOfBytes += lstrlen(DllName) + 1;
return TRUE;
}
/*----------------------------------------------------------------*/
// 過程: GetDLLs()
// Description.: 封裝串的回調
/*----------------------------------------------------------------*/
BOOL CALLBACK GetDLLs(IMAGEHLP_STATUS_REASON Reason, LPSTR ImageName,
LPSTR DllName, ULONG Va, ULONG Parameter)
{
if(Reason == BindImportModule || Reason == BindImportModuleFailed)
{
lstrcpy(*g_ppszBuf, DllName);
*g_ppszBuf += lstrlen(*g_ppszBuf) + 1;
g_iNumOfDLLs++;
}
return TRUE;
}
最後,這些函數由 DepList.def 文件輸出:
EXPORTS
GetImportTableSize @1
GetImportTable @2
如今,你能夠編譯咱們給出的全部代碼了。
註冊擴展
這個清單顯示了須要添加到ATL腳本ExeMenu.rgs末尾的修改代碼,以便註冊咱們的Shell擴展。
Exefile
{
Shellex
{
ContextMenuHandlers
{
{20349851-699F-11D2-9DAF-00104B4C822A}
}
}
}
Dllfile
{
Shellex
{
ContextMenuHandlers
{
{20349851-699F-11D2-9DAF-00104B4C822A}
}
}
}
}
改變以後,在下一次啓動Shell時,你將發現由右鍵在EXE和DLL文件上生成的關聯菜單有一個新的‘相關列表’項。這是咱們的Shell擴展給出的。
添加新查找菜單
產生關聯菜單擴展的另外一個值得注意的用途是定製顯示‘查找’菜單的列表,例如,咱們能夠添加查找全部當前運行中進程的工具。
假若咱們已經有了一個添加了新的‘查找’實用程序的關聯菜單,則要作的只是寫幾個註冊表信息段:
在你所看到的靜態鍵下,須要添加新鍵FindProcess,並使之成爲根的新子鍵。這個鍵的默認值必須是一個關聯菜單擴展的CLSID。在它的下面,鍵名爲0 的默認值是顯示在菜單上的串。最後經過添加0鍵的DefaultIcon子鍵,能夠爲這個菜單項分配圖標。
稍微思考一下,咱們將看到這是一個陌生的並且是最小的Shell擴展。不須要任何初始化,由於沒有要操做的文件。不須要描述,由於沒有狀態條,甚至不須要顯式添加新項,由於Shell在讀註冊表時做了這個工做。事實上咱們須要Shell擴展來定製‘查找’菜單一點也不神祕。
由於‘查找’菜單也是經過探測器導出的,你可能覺得描述是必須的,可是通過快速測試已經存在的菜單項後,咱們知道,不是這樣。創建關聯菜單Shell擴展的複雜性減小到僅僅實現InvokeCommand()方法,這是一個導出實際運行查找實用程序的函數。
設置註冊表
編寫關聯菜單的Shell擴展做爲新的‘查找’實用程序工做只需很是小的努力,就象下面代碼說明的同樣。這裏是在ExeMenu.h實現的四個接口方法須要做一點工做:
// QueryContextMenu
HRESULT CProcess::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
UINT idCmdLast, UINT uFlags)
{
return S_OK;
}
// InvokeCommand
HRESULT CProcess::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
m_Dlg.DoModal();
return S_OK;
}
// GetCommandString
HRESULT CProcess::GetCommandString(UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszText, UINT cchMax)
{
return S_OK;
}
// Initialize
HRESULT CProcess::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hKeyProgID)
{
return S_OK;
};
有點複雜的是構造註冊表的腳本。注意下面的擴展,不要置換出時的腳本。這段代碼應該加到ATL給出的RGS末尾。
HKLM
{
Software
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
FindExtensions
{
Static
{
FindProcess = s '{977DA8D2-41D5-11D2-BC00-AC6805C10E27}'
{
0 = s 'Find &Process...'
{
DefaultIcon = s '%MODULE%,0'
}
}
}
}
}
}
}
}
}
}
查找運行中的進程
枚舉運行進程在Windows9x和NT下要求不一樣的技術——前者在ToolHelp.dll中提供了有價值的函數集,然後者沒有。在NT下,你必須藉助於另外一個有至關差異的庫PSAPI.dll。這個庫與NT4.0一同發佈,但並不老是拷貝到你的硬盤上,不過在VC++ 的CD上你將能找到兩個文件psapi.h 和 psapi.lib。
咱們不打算細說這個過程,由於它超出了本書的範圍,你能夠參考MSDN知識庫的文章。
IContextMenu2 和 IContextMenu3接口
在IE4.0中增長了兩個關聯菜單的接口,兩者都是在IContextMenu上進行的改進。更精確地講,IContextMenu2是對IContextMenu的擴充,而IContextMenu3(要求IE4.0)是對IContextMenu2的加強。然而,這兩個接口僅僅比IContextMenu多了一個函數。這個額外的函數在IContextMenu2中爲HandleMenuMsg(),而在IContextMenu3中反而爲HandleMenuMsg2(),這就令人更容易混淆了。其原型相似於:
HRESULT HandleMenuMsg(UINT uMsg,
WPARAM wParam,
LPARAM lParam);
HRESULT HandleMenuMsg2(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
LRESULT* plResult);
這兩個接口經過提供自繪製(位圖)關聯菜單,對IContextMenu進行了擴充。尤爲,HandleMenuMsg()能夠解釋和處理三個系統消息:
WM_INITMENUPOPUP
WM_MEASUREITEM
WM_DRAWITEM
後兩個消息僅在有自繪製菜單項時才起做用。對此,HandleMenuMsg2()增長了第四個消息:WM_MENUCHAR。這個科目的資料能夠在Internet客戶端SDK中找到。
右鍵拖拽
Windows Shell提供了從一個目錄拖拽文件到另外一目錄的可能性,可是若是你使用鼠標右鍵執行這個操做時,這個行爲就被修改了:有一個菜單來提示你。這並非最有用的Windows特徵,可是它容許你決定在拖拽文件對象集以後要作什麼:
象圖中顯示的那樣,Windows提供了一種典型的操做菜單。同時還考慮到做爲活動結果,什麼操做是正確的——例如,若是你在同一個源文件夾內拖拽,就沒有‘Move Here’菜單項。所以右鍵拖拽不支持鍵盤修改器操做,如Ctrl 或 Shift按鍵,它們容許快速改變操做結果。全部可用的操做都在最終的菜單上列出。
你也能夠在此添加客戶項——一個普通的關聯菜單擴展就夠了。然而,即便拖拽處理器和關聯菜單處理器在編程上看是同一個東西,可是在註冊時它們仍是有至關的差異。
註冊拖拽處理器
右鍵拖拽處理器並非在基本文件類型上工做,所以,你不能安裝它來單獨處理如ZIP這樣文件。它們僅僅應用於目錄,下面是一個典型的註冊腳本,其中咱們註冊右鍵拖拽處理器在目錄內容上工做。
HKCR
{
Directory
{
Shellex
{
DragDropHandlers
{
RightDropDemo = s '{20349851-699F-11D2-9DAF-00104B4C822A}'
}
}
}
}
頭一件要注意的是,你的註冊表條目是在DragDropHandlers鍵下,不是ContextMenuHandlers。進一步,你須要創建特殊的子鍵並設置默認值爲接口的CLSID。子鍵的名字並不重要。探測器將枚舉所有DragDropHandlers樹的內容。
一般這個擴展的頭一個被調用的方法是IShellExtInit::Initialize(),在這裏你能夠檢查選中文件的類型。輸入的變量分別給出用戶拖拽的目標文件夾的PIDL,數據對象(以此能夠恢復被操做文件),和包含具備焦點文件的文件類型信息的註冊鍵。
經過檢查文件擴展名,你能夠避免對不但願或沒必要要的文件進行操做。這徹底不一樣於咱們前面所做的。對於拖拽處理器,在同一棵樹上註冊全部Shell擴展,以及在初始化期間就能夠決定是否對選中的文件感興趣。要終止這個Shell擴展,只須要簡單地從Initialize()返回E_FAIL便可。下面是一個例子,其中咱們假設一個類CDropExt實現了IContextMenu 和IShellExtInit接口:
STDMETHODIMP CDropExt::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hkeyProgID)
{
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM medium;
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_FAIL;
TCHAR szFile[MAX_PATH] = {0};
HDROP hdrop = static_cast<HDROP>(medium.hGlobal);
// 取得拖拽的文件數
UINT cFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
// 依次處理文件
for(int i = 0 ; i < cFiles ; i++)
{
// 取得第i個文件名
DragQueryFile(hdrop, i, szFile, MAX_PATH);
// 檢查擴展名和返回 E_FAIL 來終止
}
return S_OK;
}
在上面代碼中,咱們掃視了拖拽文件列表(經過IDataObject得到的),依次取得每個文件的名字,而且檢查其擴展名以決定它是不是所支持的類型。
假若右鼠標鍵執行了操做,右鍵拖拽處理器是在拖拽操做的源文件上工做的。這不一樣於DropHandler示例,它應用於拖拽活動的目標。
若是你查看一下你的PC註冊表內容,就會發現,沒有象關聯菜單處理那樣對給定文件類型註冊的拖拽擴展。WinZip這個少有的實用程序以這種方式工做:當你右鍵拖拽文件時,它的擴展老是在後臺工做,僅在你拖拽了ZIP文件後它才彈出。
指派動態圖標
咱們到目前爲止討論的屬性頁和關聯菜單是兩個具備挑戰性的通用Shell擴展應用,但它們並非僅有的。這一節咱們將介紹動態圖標。即,討論給定同文件類型的不一樣文件以不一樣的圖標。
考慮EXE文件,每當在Shell觀察中遇到它們時,所顯示的圖標都不是那種文件類型的通常圖標,而是屬於文件自己的圖標(固然,除非這個EXE不包含圖標)。甚至對ICO文件也是如此。
事實上這是自Windows95以來的Shell特徵,因此頗有可能你從未過多地考慮過它。然而動態指派圖標到必定類型的文件是Shell經過Shell擴展提供的確切行爲。咱們下面就介紹一個例子,它向你展現怎樣應用這個技術到BMP文件。這裏展現的並非對任何位圖的16x16像素圖片的預覽——壓縮800x600真彩圖象到小圖標是一項痛苦的活動。咱們所要作的是在視野中使用圖標來提供位圖的信息,以及怎樣使不一樣的圖標來適應BMP文件的調色板。
不一樣顏色深度的圖標
基本上,咱們打算區別四種狀況,並指派不一樣的圖標:
單色位圖
16色(4-位t)
256色(8-位)
真彩色位圖(24-位或更大)
想法是定義IconHandlerShell擴展(並放置到註冊表鍵),使它來檢查每個位圖文件的色彩表,以便返回正確的圖標到探測器顯示。IconHandlerShell擴展要求實現下面的COM接口:
IExtractIcon
IPersistFile
頭一個是在模塊與探測器之間進行通信的工具。換句話說,探測器將調用IExtractIcon的方法來請求經過IPersistFile接口裝入的文件要顯示的圖標。
注意,因爲這個擴展不只應用於選中文件,並且是任何文件,所以初始化是由IPersistFile而不是IShellExtInit執行的。
初始化圖標處理器擴展
IPersistFile接口在IUnknown之上由六個函數組成,其原形以下:
HRESULT GetClassID(LPCLSID lpClsID);
HRESULT IsDirty();
HRESULT Load(LPCOLESTR pszFileName, DWORD dwMode);
HRESULT Save(LPCOLESTR pszFileName, BOOL fRemember);
HRESULT SaveCompleted(LPCOLESTR pszFileName);
HRESULT GetCurFile(LPOLESTR* ppszFileName);
由於咱們知道這個Shell擴展的目的,所以並不須要實現全部這些方法。事實上,只Load()方法就足夠了,其它方法,咱們將只返回E_NOTIMPL。Load()方法存儲須要圖標的位圖文件名,因此咱們所要作的是轉換Unicode文件名到ANSI串,並把它存儲到要進一步使用的數據成員中。
恢復圖標
探測器取得顯示圖標有兩種可能的方法,而每一種方法都經過IExtractIcon傳遞,它們是:
GetIconLocation()
Extract()
頭一個返回要使用的圖標路徑和索引,使用一些標誌來向Shell說明怎樣處理它。相反,探測器調用第二種方法以給這個擴展一個機會來抽取圖標自己。如今讓咱們從GetIconLocation()開始更詳細地說明一下:
HRESULT GetIconLocation(UINT uFlags, // 須要圖標的理由
LPSTR szIconFile, // 含有圖標路徑名的緩衝
INT cchMax, // 緩衝尺寸
LPINT piIndex, // 包含圖標索引的整數指針
UINT* pwFlags); // 發送關於圖標的信息到Shell
uFlags對咱們來說並非特別有用,可是若是操做文件夾或通常的文件而不是位圖,它多是有用的——其中,它可使你知道是否要求一個‘打開’狀態的圖標。
另外一個標誌參數pwFlags,容許咱們告訴Shell下面幾點:
標誌 |
描述 |
GIL_DONTCACHE |
防止探測器將圖標存入其內部緩存 |
GIL_NOTFILENAME |
經過szIconFile和piIndex傳遞的信息內有封裝爲<路徑,索引>對。 |
GIL_PERCLASS |
這個圖標應該被用於這個類的任何文檔。在咱們的例子中這個標誌沒有使用,由於咱們想要得到要求的圖標。若是想要指派文件類的圖標,微軟推薦使用註冊表(參見第14章) |
GIL_PERINSTANCE |
這個圖標被指派給特定的文檔。這個類中的每個文檔都有本身的圖標。這正是咱們想要的。 |
GIL_SIMULATEDOC |
這是創建文檔所須要的圖標 |
當探測器須要顯示文件圖標時,它首先查找註冊的IconHandler擴展,若是找到,就經過調用IPersistFile::Load()函數使用給定的文件初始化這個模塊。而後,它經過調用IExtractIcon::GetIconLocation()請求擴展提供圖標的路徑名和索引。探測器如今但願接收全部須要恢復圖標的信息,若是GetIconLocation()失敗,Shell繼續在找到的下一個擴展上操做。GetIconLocation()成功,則返回S_OK,若是返回S_FALSE,Shell將使用在DefaultIcon註冊表鍵下指定的默認圖標。GetIconLocation()返回後探測器檢查pwFlags變量。若是GIL_NOTFILENAME位打開,這說明擴展想要本身抽取圖標。它就調用Extract()方法,並傳遞從szIconFile和
piIndex中接收來的信息。探測器但願從Extract()中接收一對HICONs爲小圖標和大圖標,其定義是:
HRESULT Extract(LPCSTR pszFile, // 由GetIconLocation經過szIconFile返回的值
UINT nIconIndex, // 由GetIconLocation經過piIndex返回的值
HICON* phiconLarge, // 指向接收大圖標 Handle 的 HICON 指針
HICON* phiconSmall, // 指向接收小圖標 Handle 的HICON 指針
UINT nIconSize); // 指望的圖標像素尺寸低字爲大圖標,高字爲小圖標
這個函數必須確保探測器得到文件的大小圖標的Handle。更重要的,這個函數應該返回S_FALSE來防止探測器本身抽取圖標。在絕大多數狀況下你不須要實現Extract(),可是你應該指派它返回S_FALSE而不是E_NOTIMPL。
詳細示例
爲了說明這項技術,咱們打算創建一個命名爲BmpIcons的ATL DLL工程(project)。下圖顯示咱們用於表示各類位圖的圖標,你固然能夠在本身的實現中自由調換它們:
這四個圖標分別表示單色,16色,256色,和真彩色。把它們做爲資源加入到咱們的工程中,命名爲BmpMono.ico,Bmp16.ico,Bmp256.ico 和 Bmp24.ico。
而後添加一個簡單的對象Icon 到工程中。所生成的CIcon類須要從IExtractIconImpl和 IPersistFileImpl導出,這兩個ATL類提供了IExtractIcon和IPersistFile接口的基本實現:
// IExtractIconImpl.h
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IExtractIconImpl : public IExtractIcon
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IExtractIconImpl)
// IExtractIcon
STDMETHOD(Extract)(LPCSTR, UINT, HICON*, HICON*, UINT)
{
return S_FALSE;
}
STDMETHOD(GetIconLocation)(UINT, LPSTR, UINT, LPINT, UINT*)
{
return S_FALSE;
}
};
對於咱們而言,Extract()這裏定義的是完美的——咱們並不須要在CIcon源碼中重載它。反回來考慮IPersistFile接口,咱們能夠把全部東西都放在‘Impl’類中,以提升它的可重用性:
// IPersistFileImpl.h
#include <AtlCom.h>
class ATL_NO_VTABLE IPersistFileImpl : public IPersistFile
{
public:
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IPersistFileImpl)
// IPersistFile
STDMETHOD(GetClassID)(LPCLSID)
{
return E_NOTIMPL;
}
STDMETHOD(IsDirty)()
{
return E_NOTIMPL;
}
STDMETHOD(Load)(LPCOLESTR wszFile, DWORD /*dwMode*/)
{
USES_CONVERSION;
lstrcpy(m_szFile, OLE2T(wszFile));
return S_OK;
}
STDMETHOD(Save)(LPCOLESTR, BOOL)
{
return E_NOTIMPL;
}
STDMETHOD(SaveCompleted)(LPCOLESTR)
{
return E_NOTIMPL;
}
STDMETHOD(GetCurFile)(LPOLESTR*)
{
return E_NOTIMPL;
}
};
咱們的Shell擴展聲明以下:
#include "resource.h"
#include "IPersistFileImpl.h"
#include "IExtractIconImpl.h"
#include <comdef.h>
//////////////////////////////////////////////////////////////////////////
// CIcon
class ATL_NO_VTABLE CIcon :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CIcon, &CLSID_Icon>,
public IExtractIconImpl,
public IPersistFileImpl,
public IDispatchImpl<IIcon, &IID_IIcon, &LIBID_BMPICONSLib>
{
public:
CIcon()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_ICON)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CIcon)
COM_INTERFACE_ENTRY(IIcon)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IExtractIcon)
END_COM_MAP()
// IExtractIcon
STDMETHOD(GetIconLocation)(UINT, LPSTR, UINT, LPINT, UINT*);
// IIcon
public:
private:
int GetBitmapColorDepth();
};
如今咱們只需給出GetIconLocation()函數塊便可,這是圖標處理器的核心函數。咱們還添加了私有的輔助函數GetBitmapColorDepth()。
HRESULT CIcon::GetIconLocation(UINT uFlags, LPSTR szIconFile, UINT cchMax,
LPINT piIndex, UINT* pwFlags)
{
// 存儲咱們本身的圖標
::GetModuleFileName(_Module.GetModuleInstance(), szIconFile, cchMax);
// 解析位圖色彩表
int iBitCount = GetBitmapColorDepth();
if(iBitCount < 0)
return S_FALSE;
switch(iBitCount)
{
case 1:
*piIndex = 0; // 單色
break;
case 4:
*piIndex = 1; // 16 色
break;
case 8:
*piIndex = 2; // 256 色
break;
default:
*piIndex = 3; // 真彩色
}
*pwFlags |= GIL_PERINSTANCE | GIL_DONTCACHE;
return S_OK;
};
int CIcon::GetBitmapColorDepth()
{
// 讀文件頭
HFILE fh = _lopen(m_szFile, OF_READ);
if(fh == HFILE_ERROR)
return -1;
BITMAPFILEHEADER bf;
_lread(fh, &bf, sizeof(BITMAPFILEHEADER));
BITMAPINFOHEADER bi;
_lread(fh, &bi, sizeof(BITMAPINFOHEADER));
_lclose(fh);
// 返回
return bi.biBitCount;
};
到此這個Shell擴展的源碼就完成了,可是咱們還要考慮它的註冊問題。與任何其它Shell擴展同樣,若是在註冊期間遺漏了某些東西,將不能使這個擴展正常工做。
註冊圖標處理器
圖標處理器的Shell擴展與其它Shell擴展遵循同樣的模式,然而它使用不一樣的鍵。此時咱們須要在被喚醒文檔類的ShellEx鍵下創建IconHandler鍵。對於位圖(若是使用微軟的‘圖畫’打開它們),其鍵爲:
HKEY_CLASSES_ROOT
/Paint.Picture
/ShellEx
/IconHandler
而後把它的默認值設置爲對象的CLSID,而且還應該設置DefaultIcon鍵到%1,以使探測器知道圖標應該逐個文件肯定。正常狀況下,DefaultIcon鍵包含文件名和索引組成的逗號分隔的串。下面是ATL生成腳本的非標準部分:
HKCR
{
// 對象註冊
Paint.Picture
{
DefaultIcon = s '%%1'
ShellEx
{
IconHandler = s '{A2B00480-425A-11D2-BC00-AC6805C10E27}'
}
}
}
注意,DefaultIcon鍵所取的值%1須要兩個百分號(%%)。
爲了確保正常工做,最安全的辦法就是重啓機器或註銷登陸。注意老的DefaultIcon鍵值被覆蓋,因此你應該把它保存在一個安全的地方。下圖顯示了你所看到的Shell是怎樣由擴展所改變的:
同一個文件類不能有多個IconHandler擴展。若是註冊了多個,僅第一個被考慮。
經過ICopyHook監視文件夾
許多程序員的夢想是可以編寫實用程序來監視文件系統發生的事件。確定有許多理由要這麼作,可是測試應用,排錯和知足好奇心也必定是其中的緣由。
在第7章中咱們討論了通知對象,它通知你的應用在文件系統中或指定文件夾內某些東西發生了變化。不幸的是在Windows95和98下,沒有辦法知道那一個文件引發通知發生。換句話說,你知道了在被監視的文件夾下某些東西發生了變化,然後則徹底要你來確切地描繪發生了什麼。在NT下這個事情就稍微好了一點,這要感謝平臺專用的函數ReadDirectoryChangesW()。
即便有必定數量的NT函數隨Windows98一塊兒輸出到了Windows9x平臺,ReadDirectoryChangesW()函數仍是沒在其中。帶之的是 MoveFileEx(),CreateFiber()和CreateRemoteThread()在Windows98下是可用的。
Windows Shell的幫助說明一個稱之爲ICopyHook的接口,能夠用於執行相似的操做。基本上,它能監視發生在文件夾內的拷貝,移動,重命名和刪除操做。看上去確實夠刺激的,可是很不幸,有三個嚴重的缺陷限制了這個擴展的使用:
它僅應用於文件夾和打印機,不能對文件類型
它僅能使你容許或禁止操做,不能本身執行它
它僅是你知道操做何時開始,不能知道它何時結束
做爲另外一個例子咱們打算創建一個ATL工程(project)來講明怎樣實現這個接口和創建一個目錄監視工具。
實現ICopyHook接口
對於這個例子,咱們使用ATL COM應用大師創建一個‘Copy’工程(project),接受全部默認的選項,生成以後,使用對象大師添加一個簡單的ATL對象‘Monitor’,並對其頭文件Monitor.h作一些修改:
#include "resource.h"
#include "ICopyHookImpl.h"
/////////////////////////////////////////////////////////////////////
// CMonitor
class ATL_NO_VTABLE CMonitor :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMonitor, &CLSID_Monitor>,
public IShellCopyHookImpl,
public IDispatchImpl<IMonitor, &IID_IMonitor, &LIBID_COPYLib>
{
public:
CMonitor()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_MONITOR)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_IID(IID_IShellCopyHook, CMonitor)
END_COM_MAP()
// ICopyHook
public:
STDMETHOD_(UINT, CopyCallback)(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs);
// IMonitor
public:
};
你可能已經注意到COM映射與咱們前面的例子有點不一樣,這是由於新的COM_INTERFACE_ENTRY_IID()宏,咱們過一會再討論它。CMonitor類從IShellCopyHookImpl類中導出,依次繼承於ICopyHook:
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IShellCopyHookImpl : public ICopyHook
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellCopyHookImpl)
// ICopyHook
STDMETHOD_(UINT, CopyCallback)(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs);
};
在前面的例子中已經看到,絕大多數過程看上去都是很是類似的。ICopyHook接口要求你實現單一個函數CopyCallback(),這基本上是創建在SHFileOperation()之上的濾波函數(參見第3章)。它捕捉全部經過那個函數的操做,而你的實現能夠容許或拒絕它們發生。CopyCallback()函數與SHFileOperation()函數十分相像是不奇怪的,
UINT CopyCallback(HWND hwnd, // 處理器顯示全部窗口的父窗口
UINT wFunc, // 要執行的操做
UINT wFlags, // 操做屬性
LPCSTR pszSrcFile, // 操做源文件
DWORD dwSrcAttribs, // 源文件的DOS屬性
LPCSTR pszDestFile, // 操做的目標文件
DWORD dwDestAttribs); // 目標文件的DOS屬性
CopyCallback()返回UINT值,它封裝了典型的MessageBox()返回內容:IDYES, IDNO, IDCANCEL。操做是繼續仍是拒絕,或被取消依賴於這個返回值。拒絕的意思是隻是這個操做不被執行,相反,取消則是全部相關操做都將被取消。
ICopyHook接口的IID
在開發咱們的第一個CopyHook擴展期間,咱們規定ICopyHook的接口ID爲IID_ICopyHook,所以編譯器在編譯時有一個未聲明標識符錯,奇怪ICopyHook的IID不是IID_ICopyHook,而是IID_IShellCopyHook。
這實際引發ATL代碼的一個問題,聲明COM服務器對象的映射問題。在添加新的Monitor對象到ATL工程後,頭文件的代碼有以下形式:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
COM映射對應於對象的QueryInterface()的實現,因此爲了輸出ICopyHook接口,以及參考其它例子在這一點所作的,咱們像這樣添加了以行代碼:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ICopyHook)
END_COM_MAP()
咱們說過這能引發編譯錯,爲此咱們必須通知ATL這個接口輸出ICopyHook,可是它的IID不是IID_ICopyHook。幸運地是,ATL設計者已經預先清除了這個問題,有一個COM_INTERFACE_ENTRY宏確切地處理這種狀況:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_IID(IID_IShellCopyHook, CMonitor)
END_COM_MAP()
這個宏告訴ATL使用第二個參數命名的類的虛表(vtable)做爲第一個參數所表示的接口的實現。這正好是咱們所須要的。
記錄操做
咱們打算在這裏創建的擴展簡單地組成和輸出串到Log文件。這些串包含了源目文件名,操做類型,和發生的時間。下面是CopyCallback()的實現:
UINT CMonitor::CopyCallback(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs)
{
TCHAR szTime[50] = {0};
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 50);
FILE* f = NULL;
f = fopen(__TEXT("c://monitor.log"), __TEXT("a + t"));
fseek(f, 0, SEEK_END);
switch(wFunc)
{
case FO_MOVE:
fprintf(f, __TEXT("/n/n/rMoving:/n/r[%s] to [%s]/n/rat %s"),
pszSrcFile, pszDestFile, szTime);
break;
case FO_COPY:
fprintf(f, __TEXT("/n/n/rCopying:/n/r[%s] to [%s]/n/rat %s"),
pszSrcFile, pszDestFile, szTime);
break;
case FO_DELETE:
fprintf(f, __TEXT("/n/n/rDeleting:/n/r[%s] /n/rat %s"),
pszSrcFile, szTime);
break;
}
fclose(f);
// 不妨礙正常的控制流
return IDYES;
}
註冊CopyHook擴展
要註冊CopyHook擴展,咱們須要在想要鉤住的文件類型鍵的ShellEx鍵下創建CopyHookHandlers鍵。在CopyHookHandlers下創建一個新鍵,名字能夠是任何喜歡的名字——Shell簡單地枚舉全部它找到的子鍵。其默認值應該指向這個擴展的CLSID。
下面是ATL註冊腳本代碼的補充(咱們取Monitor做爲鍵名):
HKCR
{
// 對象註冊
Directory
{
ShellEx
{
CopyHookHandlers
{
Monitor = s '{7842554E-6BED-11D2-8CDB-B05550C10000}'
}
}
}
}
此時咱們註冊了一個在目錄上工做的擴展。你能夠試着把它註冊爲應用於文件類型(如exe文件),此時Shell將不能喚醒這個擴展。這是設計行爲。
下面顯示了典型的Log文件:
可監控對象
儘管咱們有關於文件的報警,目錄也不是CopyHook擴展能夠監視的惟一對象——打印機和驅動器也能夠監視。要鉤住打印機,你須要註冊服務器到HKEY_CLASSES_ROOT/Printers鍵下,這就是容許打印機管理器在每次打印時彈出它們本身的用戶界面的竅門。
在Internet 客戶端SDK的資料中說明,你能夠註冊CopyHook擴展在*.鍵下,這使你相信能監視文件操做,可是不幸的是這不是真的。就咱們的經驗,沒有可以查詢單個文件是被拷貝仍是被移動的辦法。
關於拷貝鉤子的進一步說明
咱們前面說過,Shell並不通知你的擴展鉤子操做的結果(成功,失敗,中斷)。然而由於你知道操做喚醒的目錄,你可使用通知對象(見第7章)來試着感受它。經過在原路徑上安裝這個對象,你就能夠知道何時某些事情發生了變化。而後經過檢查,你還能發現事情怎麼發生的變化。例如,對於拷貝,你能夠驗證目標目錄是否包含了與源同名的文件。
實際,這並不太容易,由於 SHFileOperation()(鉤子後面的函數)容許衝突文件重命名。因此係統指派的目標是不一樣於源名的。這是十分合理的。
在咱們開發其基本文檔由文件集組成的產品時就開始研究CopyHook擴展了。若是咱們的客戶想要經過Shell(而不是程序)管理文檔,他們就必須記住文檔的內部結構,以確保拷貝或刪除它的全部部件。咱們的想法就是鉤住拷貝,移動,重命名和刪除操做,這樣才能保證在其中的任何一個變化時全部相關的文件都被影響。然而,正像咱們說過的那樣,這彷佛是不可能的,因此咱們所開發的程序最終使用了複合文件和OLE結構的存儲。
拖拽數據到文件
Win32程序的通用特徵是容許從探測器窗口選擇文件,拖拽它們到程序的客戶區域,以及使它感受和處理所接收的數據。在前面章中咱們已經給出了這方面的示例,特別是第6章。
咱們將在這裏給出另外一種有點不一樣的方法。對於探測器窗口或Windows桌面上必定類型的單個文件,咱們想要使它可以處理一樣的拖拽事件。頭一個例子就是WinZip:若是拖拽一個或多個文件到存在的.zip文件上,鼠標將變成‘+’形式(十字線),一旦落下,拖拽文件就被壓縮並加到這個存檔文件中。這個行爲是經過另外一種類型的Shell擴展得到的:DropHandler。
拖拽處理器擴展
‘DropHandler’擴展由IDropTarget 和 IPersistFile導出,必須註冊在下面鍵下:
HKEY_CLASSES_ROOT
/<FileClass>
/ShellEx
/DropHandler
這裏的<FileClass>顯然是想要擴展處理的文檔類型標識符名。
一般Default值應該是這個服務器的CLSID,注意,DropHandler不容許多重處理器同時操做同一個文件類型。也就是說,註冊鍵的名字是不能重複的。
IDropTarget接口
在給出示例以前,須要查看一下IDropTarget接口的方法。它們在拖拽發生之後,和鼠標環繞可能的目標移動時均可能被涉及到:
方法 |
描述 |
DragEnter() |
鼠標進入一個可能的目標,這個目標決定數據是否能夠被接受 |
DragOver() |
鼠標在一個可能的目標上移動 |
DragLeave() |
鼠標離開了拖拽區域 |
Drop() |
拖拽已經完成 |
通常狀況下,對於OLE拖拽操做的可能目標是一個窗口(或窗口的一部分),它是經過RegisterDragDrop()函數自注冊的。當組織和管理拖拽(源)的模塊感受到鼠標下有一個窗口時,它將覈實是否有拖拽支持存在,若是存在,源則得到一個指向這個窗口輸出的IDropTarget接口指針,然後開始調用上述方法。
在這個鉤子下,就是簡單地檢查是否這個HWND有包含指向IDropTarget接口的指針屬性。這裏的屬性是一個32位的Handle,它是經過SetProp() API函數附着在這個窗口上的。
當鼠標進入潛在的目標區域後,DragEnter()得到調用。IDropTarget接口老是與窗口關聯,可是,經過適當地編碼DragEnter(),你能夠把任何區域做爲可能的拖拽目標。這個方法的原型是:
HRESULT IDropTarget::DragEnter(LPDATAOBJECT pDO,
DWORD dwKeyState,
POINTL pt,
DWORD* pdwEffect);
它接收指向IDataObject接口的指針,這個接口包含了被拖拽的數據。另外的參數是32位值,它們表示鍵盤狀態,鼠標在屏幕上的位置座標,和用操做所容許的結果充填的緩衝。換句話說,這個方法指望解析接收的數據,鼠標位置和鍵盤狀態,以便肯定它是否能接受此次拖拽。使用這種方式,你就可以僅接受必定窗口區域上的拖拽操做。(你須要轉換客戶座標位置),在鼠標移動到目標區域上時調用DragOver()方法。這個方法提供了拖拽操做的實時信息——隨着鼠標的移動,最終結果可能改變,其原型考慮到了鼠標位置和鍵盤狀態:
HRESULT IDropTarget::DragOver(DWORD dwKeyState,
POINTL pt,
DWORD* pdwEffect);
再次注意,你能夠用32位輸出緩衝通知鼠標所指望的結果。固然,DragOver()被更頻繁地調用,而且老是在DragEnter()以後。所以由DragOver()設置的結果能夠覆蓋由DragEnter()所設置的。DragLeave()是一個很是簡單的方法,它使目標知道鼠標已經退出它的領域,其原型爲:
HRESULT IDropTarget::DragLeave();
最後一個方法,Drop(),當數據在目標上被釋放時,得到調用。這顯然是全部方法中最重要的方法。所以也有更多須要說明的。爲了創建ATL部件,咱們須要在下面的例子中使用IPersistFileImpl.h頭文件。它也提供了IDropTarget接口的基類:
// IDropTargetImpl.h
#include <AtlCom.h>
class ATL_NO_VTABLE IDropTargetImpl : public IDropTarget
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL( IDropTargetImpl )
// IDropTarget (優化的Shell拖拽對象)
STDMETHOD(DragEnter)(LPDATAOBJECT pDO, DWORD dwKeyState,POINTL pt,
DWORD *pdwEffect)
{
STGMEDIUM sm;
FORMATETC fe;
// 咱們是否接受這個類型的數據
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_HDROP;
if(FAILED(pDO->GetData(&fe, &sm)))
{
fe.cfFormat = CF_TEXT;
if(FAILED(pDO->GetData(&fe, &sm)))
{
// 拒絕拖拽
*pdwEffect = DROPEFFECT_NONE;
return E_ABORT;
}
}
// 默認活動是拷貝數據
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
STDMETHOD(DragOver)(DWORD dwKeyState, POINTL pt, DWORD* pdwEffect)
{
// 不接受鍵盤修改器
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
STDMETHOD(DragLeave)()
{
return S_OK;
}
STDMETHOD(Drop)(LPDATAOBJECT pDO, DWORD dwKeyState,POINTL pt, DWORD* pdwEffect);
};
誠然,這個基本實現是爲咱們本身的目的所優化的,事實上,在這個頭文件中的代碼除了Drop()以外,是接口的基本行爲。而這個類的另外兩個特徵是:
目標僅接受通常文本格式數據,格式名爲CF_TEXT,這是Windows剪裁板標準格式。
目標僅支持‘拷貝’操做,不支持其它操做如‘連接’或‘移動’。
處理TXT文件的拖拽事件
咱們給出的第一個例子由處理TXT文件上的拖拽文本組成。想法是拖拽數據(文件或簡單文字)將添加到目標文件的底部。從創建DropText DLL工程(project)開始。使用ATL COM應用大師和對象大師創建StrAdd對象。這個ATL對象聲明以下:
#include "resource.h" // 主符號表
#include "IPersistFileImpl.h"
#include "IDropTargetImpl.h"
#include <ComDef.h>
////////////////////////////////////////////////////////////////////////////
// CStrAdd
class ATL_NO_VTABLE CStrAdd :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CStrAdd, &CLSID_StrAdd>,
public IDropTargetImpl,
public IPersistFileImpl,
public IDispatchImpl<IStrAdd, &IID_IStrAdd, &LIBID_DROPTEXTLib>
{
public:
CStrAdd()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_STRADD)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CStrAdd)
COM_INTERFACE_ENTRY(IStrAdd)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IDropTarget)
COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()
// IDropTarget
public:
STDMETHOD(Drop)(LPDATAOBJECT, DWORD, POINTL, LPDWORD);
// IStrAdd
public:
private:
HDROP GetHDrop(LPDATAOBJECT);
BOOL GetCFText(LPDATAOBJECT, LPTSTR, UINT);
};
最值得注意的代碼部分是在落下發生時處理過程,這在Drop()中定義的,其代碼顯示以下:
#include "stdafx.h"
#include "DropText.h"
#include "StrAdd.h"
#include <shlwapi.h>
// 常量
const int MAXBUFSIZE = 2048; // 要接收的文本尺寸
const int MINBUFSIZE = 50; // 被顯示的文本尺寸
////////////////////////////////////////////////////////////////////////////
// CStrAdd
HRESULT CStrAdd::Drop(LPDATAOBJECT pDO, DWORD dwKeyState, POINTL pt,
LPDWORD pdwEffect)
{
// 獲取CF_HDROP 數據對象
HDROP hdrop = GetHDrop(pDO);
if(hdrop)
{
// 在多選狀況下僅考慮頭一個文件
TCHAR szSrcFile[MAX_PATH] = {0};
DragQueryFile(hdrop, 0, szSrcFile, MAX_PATH);
DragFinish(hdrop);
// 檢查是否爲TXT文件
LPTSTR pszExt = PathFindExtension(szSrcFile);
if(lstrcmpi(pszExt, __TEXT(".txt")))
{
MessageBox(GetFocus(),__TEXT("抱歉, 你僅能拖拽TXT文件!"),
__TEXT("拖拽文件..."),MB_ICONSTOP);
return E_INVALIDARG;
}
// 在鏈接以前確認
TCHAR s[2 * MAX_PATH] = {0};
wsprintf(s, __TEXT("Would you add /n%s/nat the bottom of/n%s?"),
szSrcFile, m_szFile);
UINT rc = MessageBox(GetFocus(), s,__TEXT("Drop Files..."),
MB_ICONQUESTION | MB_YESNO);
if(rc == IDNO)
return E_ABORT;
}
else
{
TCHAR szBuf[MAXBUFSIZE] = {0};
GetCFText(pDO, szBuf, MAXBUFSIZE);
TCHAR s[MAX_PATH + MINBUFSIZE] = {0};
TCHAR sClipb[MINBUFSIZE] = {0};
lstrcpyn(sClipb, szBuf, MINBUFSIZE);
wsprintf(s, __TEXT("Would you add/n[%s...]/nat the bottom of/n%s?"),
sClipb, m_szFile);
UINT rc = MessageBox(GetFocus(), s,__TEXT("Drop Files..."),
MB_ICONQUESTION | MB_YESNO);
if(rc == IDNO)
return E_ABORT;
}
// TO DO: 連接文字操做
return S_OK;
}
這個函數支持文字和文件名,所以你能夠拖拽或者是探測器內的TXT文件或者是文本編輯器或文字處理器的一塊文本,包括Word,記事本,字處理,甚至VC++ 編輯器。
Drop()首先執行的檢查是落下的數據類型,若是GetHDrop()方法返回可用的Handle,則數據是CF_HDROP格式的,必須經過DragQueryFile()訪問。此時,這個函數僅僅處理第一個文件,而放棄全部多選狀況下的其它文件,然而這僅僅是爲了簡化處理——不妨礙你作更復雜的處理。然後這段代碼使用PathFindExtension()函數,檢查文件的擴展名是否爲TXT,這個函數在shlwapi.dll中。
// 從LPDATAOBJECT中抽取 HDROP
HDROP CStrAdd::GetHDrop(LPDATAOBJECT pDO)
{
STGMEDIUM sm;
FORMATETC fe;
// 檢查CF_HDROP 數據
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_HDROP;
if(FAILED(pDO->GetData(&fe, &sm)))
return NULL;
else
return static_cast<HDROP>(sm.hGlobal);
}
若是落下的數據不是CF_HDROP格式的,則Drop()方法使用輔助函數GetCFText()努力從其中抽取簡單文字。若是成功,它使用最大字節數填寫緩衝。
// 從LPDATAOBJECT中抽取 CF_TEXT
BOOL CStrAdd::GetCFText(LPDATAOBJECT pDO, LPTSTR szBuf, UINT nMax)
{
STGMEDIUM sm;
FORMATETC fe;
// 檢查 CF_TEXT 數據
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_TEXT;
if(FAILED(pDO->GetData(&fe, &sm)))
return FALSE;
else
{
LPTSTR p = static_cast<LPTSTR>(GlobalLock(sm.hGlobal));
lstrcpyn(szBuf, p, nMax);
GlobalUnlock(sm.hGlobal);
return TRUE;
}
}
這段C++代碼完成了這個擴展技術,下面要考慮的是註冊問題,下面是適當的註冊條目:
HKCR
{
// 對象註冊
txtfile
{
Shellex
{
DropHandler = s '{AE62DAAC-509C-11D2-BC00-AC6805C10E27}'
}
}
}
下圖顯示在拖拽TXT文件或簡單文字到一個Windows Shell中的TXT文件時顯示的確認消息框:
你會發現咱們並無給出實際連接文字的代碼,然而,這段代碼的重要部分是感受,而不是實際執行。
增長Shell對腳本的支持
在第13章中,咱們討論了Windows腳本環境對象模型,在結尾處討論對WSH環境的改進方法時,咱們提到過寫一個Shell擴展在VBScript或Jscript上拖拽參數的可能性。如今咱們就揭開這個祕密,看一下怎樣來實現它。
這是擴展是一個DropHandler,它與VBS和JS文件關聯。代碼框架與前一個例子絕對一致:ATL COM對象實現IPersistFile和 IDropTarget,重用基本的接口實現。須要改變的是Drop()方法和註冊腳本。
工程與註冊腳本
創建簡單的工程(project)VBSDrop,添加對象WSHUIDrop,改變它的RGS腳本:
HKCR
{
// 對象註冊
vbsfile
{
Shellex
{
DropHandler = s '{E671DB13-4D41-11D2-BC00-AC6805C10E27}'
}
}
jsfile
{
Shellex
{
DropHandler = s '{E671DB13-4D41-11D2-BC00-AC6805C10E27}'
}
}
}
腳本文件上的拖拽參數
咱們將要介紹的這個例子顯示怎樣經過Shell傳遞參數到VBS或JS腳本文件。這個想法是,選擇數據——例如,在探測器中的文件名——並拖拽到經過命令行接收它們的腳本文件上。這個示例使用文件名和CF_HDROP格式,可是不只限於此,你還能夠處理串。
咱們在這裏介紹的Drop()函數抽取拖拽到VBS或JS文件上的各個文件名,並創建登記條目串,其中,每個都由空格分隔,是全路徑文件名。若是路徑名包含空格,則把它們括在引號中。對最後的這個操做,咱們開發另外一個函數以排除shlwapi.dll對路徑名的限制:PathQuoteSpaces()把路徑名封裝在引號中,若是它是含有空格的長文件名。
當串準備好後,它必須做爲變量傳遞到腳本文件並執行之,這正是ShellExecute()要作的。在隨後的調用中,腳本文件名在Shell擴展初始化時被存儲在CWSHUIDrop類的m_szFile成員中:
ShellExecute(GetFocus(), __TEXT("open"), m_szFile, pszBuf, NULL, SW_SHOW);
爲了運行VBS或JS文件,你必須執行打開動詞。pszBuf變量組成了這個文件的命令行參數。
///////////////////////////////////////////////////////////////////////////
// A portion of this code also appeared in the December 1998 issue of MIND
HRESULT CWSHUIDrop::Drop(LPDATAOBJECT pDO, DWORD dwKeyState, POINTL pt,
LPDWORD pdwEffect)
{
// 獲取 CF_HDROP 數據對象
HDROP hdrop = GetHDrop(pDO);
if(hdrop == NULL)
return E_INVALIDARG;
// 取得Shell存儲處理器
LPMALLOC pMalloc = NULL;
SHGetMalloc(&pMalloc);
// 爲最終的組合串分配足夠的內存
int iNumOfFiles = DragQueryFile(hdrop, -1, NULL, 0);
LPTSTR pszBuf = static_cast<LPTSTR>(pMalloc->Alloc((1 +
MAX_PATH) * iNumOfFiles));
LPTSTR psz = pszBuf;
if(!pszBuf)
{
pMalloc->Release();
return E_OUTOFMEMORY;
}
ZeroMemory(pszBuf, (1 + MAX_PATH) * iNumOfFiles);
// 取得被拖拽文件名,並組合串
for(int i = 0 ; i < iNumOfFiles ; i++)
{
TCHAR s[MAX_PATH] = {0};
DragQueryFile(hdrop, i, s, MAX_PATH);
PathQuoteSpaces(s);
lstrcat(pszBuf, s);
lstrcat(pszBuf, __TEXT(" "));
}
DragFinish(hdrop);
// 運行腳本,傳遞拖拽文件做爲命令行變量
ShellExecute(GetFocus(), __TEXT("open"), m_szFile, pszBuf, NULL, SW_SHOW);
pMalloc->Release();
return S_OK;
}
爲了查看這段代碼的總體效果,考慮下面的Jscript代碼:
/////////////////////////////////////////////////////////////////////////
// 對Shell DropHandler 的 JScript 示例
// 它簡單地顯示命令行所接收的東西
var shell = WScript.CreateObject("WScript.Shell");
var sDrop = "Arguments received:/n/n";
if(WScript.Arguments.Length > 0)
{
for(i = 1 ; i <= WScript.Arguments.Length ; i++)
sDrop += i + ") " + WScript.Arguments.Item(i - 1) + "/n";
shell.Popup(sDrop);
}
else
shell.Popup("No argument specified.");
WScript.Quit();
把這段代碼放在jsdrop.js文件中,檢測命令行並顯示各類參數到不一樣行上列表的消息框。而後試着拖拽幾個文件到其上:
咱們新的DropHandler Shell擴展將結果列出在消息框中:
DataHandler Shell擴展
咱們就要完成Windows Shell擴展領域的旅程了,可是在結束以前,還有另外一個擴展類型須要說幾句,它涉及到另外一個通用用戶接口:剪裁板。若是想要得到對必定類型文件的‘拷貝/粘貼’操做的控制,你就應該編寫DataHandler擴展。例如,你但願在按下Ctrl-C 時改變BMP文件拷貝到剪裁板的方式,默認狀況下,Shell以CF_HDROP格式拷貝文件名。若是你但願圖像以CF_BITMAP格式進入剪裁板,則須要寫一個DataHandler擴展。
涉及的COM接口
寫DataHandler Shell擴展須要實現IPersistFile 和 IDataObject接口。這種類型的擴展註冊要求新的默認值:
HKEY_CLASSES_ROOT
/<FileClass>
/ShellEx
/DataHandler
一般你應該設置服務器的CLSID,而 <FileClass> 是這個擴展應用的文檔文件類標識符。
IDataObject負責傳遞數據到剪裁板,而且包含幾個方法。就咱們在這裏的目的,你僅須要實現星號* 標註的方法便可:
方法 |
描述 |
*GetData() |
恢復與給定對象關聯的數據 |
GetDataHere() |
相似於GetData(),可是這個函數也接收存儲數據的介質 |
*QueryGetData() |
肯定請求的數據是否可用 |
GetCanonicalFormatEtc() |
對象列出所支持的格式 |
*SetData() |
鏈接指定數據與給定對象 |
EnumFormatEtc() |
枚舉用於存儲數據於對象的格式 |
DAdvise() |
鏈接接收器對象以便知道數據變化 |
DUnadvise() |
斷開接器收對象 |
EnumDAdvise() |
枚舉當前接收器對象 |
DataHandler與IconHandler 和 DropHandler同樣不容許多重處理器同時操做同一種文件類型,也就是說,例如,你對BMP文件拷貝圖像的點到剪裁板,也就失去了拷貝文件名到剪裁板的能力——除非你的擴展實現這兩個目標。
Shell擴展開發者手冊
在這一章中,咱們使用ATL創建了必定數量的Shell擴展——事實上這已經成爲一種習慣了。下面咱們逐步說明使用ATL創建、編譯和測試Shell擴展所須要作的工做。
使用ATL COM 應用大師創建新的ATL工程(project)
添加一個簡單對象
若是沒有可用的頭,爲每個須要實現的接口寫一個IxxxImpl.h文件。你須要層次地從接口 定義新類併爲每種方法提供基本行爲。若是須要,也能夠添加屬性或私有成員。
修改新對象類的頭文件,尤爲是使它從前一步定義的全部IxxxImpl類繼承,添加接口到對象的COM映射,並添加全部須要重載的接口方法的聲明。
修改ATL註冊表腳本實現Shell擴展註冊。正常狀況,大師僅生成註冊服務器必須的代碼。
添加劇載方法的代碼
編譯這個工程,並確保註冊是所指望的。若是不能肯定,使用regsvr32.exe重複註冊
在測試功能以前必定要保證代碼被適當地裝入Shell。這個操做將依賴於Shell擴展的類型,對於關聯菜單和屬性頁,刷新探測器窗口就足夠了,而圖標處理器和拷貝鉤子,要求註銷用戶甚至重啓機器。
全部要從新編譯服務器的活動都必須首先註銷服務器。而後或者LogOff或者從新引導機器。
文件觀察器
做爲這一章的結束,咱們看一個不是Shell擴展的模塊,可是它有相同的做用。文件觀察器(也稱爲快速觀察器)是一個進程內COM服務器,它經過系統Shell添加了文檔類型功能:插入到探測器中提供快速觀察必定類型文件內容的能力。例如,Word觀察器,能夠查看Word文檔,可是遠沒有徹底的Word程序大和有力。
在一個用快速觀察器打開的文件上用戶既不能修改也不能執行特殊的功能,這個目標只簡單地提供只讀的文檔預覽沒必要導出正常的文件關聯應用。爲了給出徹底與Shell集成的文檔,文件觀察器必須與Shell擴展同樣。
文件觀察器依賴於Shell4.0之後纔可用的通用控件,但這並不老是默認安裝的——在有些PC上這個控件就可能沒有安裝,甚至這個控件都沒有在選項中出現。此時應該手動安裝和註冊。完成以後‘Quick View’項將出如今文件的關聯菜單中:
Windows有必定數量的文件觀察器,其中有一個對於觀察Win32 可執行文件(DLL/EXE)的輸出和輸入是有用的這裏就是winword.exe的活動:
開始快速觀察
在點擊‘Quick View’時,Windows導出quikview.exe程序,這是一種文件觀察管理器。它本身並不做任何工做,相反,它加載和鏈接相應實際顯示文件內容的COM模塊。
在咱們的觀點上看,文件觀察器和Shell擴展之間最大的差異在於主程序不是explorer.exe 而是 quikview.exe——文件觀察器不是運行在探測器地址空間中。此外,有一個新的COM接口與之(IFileViewer)對應,而且遵循不一樣的註冊邏輯。所以能夠說,當它加載和卸載文件觀察器時quikview.exe就象探測器同樣工做。
對於文件觀察器重要的是不只對不一樣類型的文件有不一樣的應用,並且只有一個主模塊管理全部COM擴展。這些外部插入者提供了實際的觀察功能。而且它們都註冊在下面鍵下:
HKEY_CLASSES_ROOT
/QuickView
一看你就會發現,對每個支持的文件類型都有一個鍵。下圖是典型的Windows9.x註冊表的情形:
每個特定的文件擴展名鍵都包含一個子鍵,其中含有提供顯示的COM服務器的CLSID。默認,全部支持的文件類型都在sccview.dll中實現,其CLSID是{F0F08735-0C36-101B-B086-0020AF07D0F4}。
快速觀察器怎樣得到調用
每次點擊關聯菜單或命令行‘quikview 文件名’都將引發快速觀察管理器啓動。它檢查文件擴展名,掃視註冊表的‘QuickView’註冊區域,搜索CLSID。若是成功,就創建一個COM服務器實例,並開始處理這個對象必須實現的接口。在用戶請求新的快速觀察窗口時,管理器查看‘Replace Window’工具條按鈕的狀態:
若是設置,使用相同的窗口和實例。不然,顯示觀察器的新實例。快速觀察器還須要支持拖拽,過一會咱們就會看到。
寫一個快速觀察器
快速觀察器是一個進程內COM模塊,它實現三個接口:
! IPersistFile
! IFileViewer
! IFileViewerSite
IPersistFile用於裝入指定文件。管理器只是查詢IPersistFile模塊,和調用Load()方法。典型的文件觀察器則打開文件,和轉換內容到可顯示格式。例如,若是文件是元文件,則IPersistFile::Load()可能想要創建HENHMETAFILE Handle。由於文件觀察器惟一的活動就是‘讀’,所以不須要實現整個IPersistFile接口,只編寫Load()方法就能夠很好地工做。
顯示文件
IFileViewer接口有三個函數組成:
! PrintTo()
! Show()
! ShowInitialize()
須要繪製內容的全部操做都應該寫在ShowInitialize()中。它必須創建一個不可視窗口,而且用要顯示的文件衝填其客戶域。事實上這個函數應該作顯示文件所須要的全部操做,簡化開啓創建窗口的WS_VISIBLE風格。換言之,ShowInitialize()函數須要工做在一種屏外緩衝區上。ShowInitialize()還應該注意與文件觀察器用戶界面協同操做。即:
! 創建主窗口(若是須要)
! 創建和初始化工具條和狀態條
! 設置菜單和加速器
! 創建(初始不可視)窗口來顯示內容
! 適當地縮放窗口尺寸
在任務成功地完成以後,它就轉到Show()。在其它的操做中,這個方法使窗口可視,並進入消息循環。
從這個主要描述中,咱們能夠看出,快速觀察器比插入模塊做的要多。事實上,它實際是一個編譯進DLL完整的文檔/觀察應用。你所看到的命令菜單,字體變換,甚至爲打開文件而啓動的默認應用都必須在這個DLL中處理。
快速觀察應該支持拖拽,所以窗口必須有WS_EX_ACCEPTFILES標誌。這可能引發一些狀況,例如,若是當前觀察器正在顯示一個BMP,用戶拖拽一個TXT文件到其窗口上。對於位圖觀察器怎樣設計來管理文本文件呢?
爲了管理這種狀況,背後須要作大量的工做。在解釋了這個操做以後,你就能夠理解爲何IFileViewer接口須要這兩個方法了(ShowInitialize()和Show())。頭一個方法的調用只是保證顯示文件的全部必要的事情都是可用的——若是失敗,當前顯示的文檔仍然不變。就象你從未試圖打開其它文檔同樣。這個特徵有助於使整個快速觀察應用看上去像一個總體,而不是不一樣部件的集合。
在Show()方法被調用的時候,快速觀察器接收FVSHOWINFO結構做爲單個的變量:
typedef struct
{
DWORD cbSize;
HWND hwndOwner;
int iShow;
DWORD dwFlags;
RECT rect;
LPUNKNOWN punkRel;
OLECHAR strNewFile[MAX_PATH];
} FVSHOWINFO, *LPFVSHOWINFO;
使用這個結構不只是要傳遞信息進入,並且要返回數據到quikview.exe程序。當文件被落下時,快速觀察器接收到一般的WM_DROPFILES消息,若是文件不能被處理,模塊應該作下面的工做:
設置strNewFile到實際文件名
打開dwFlags字段的FVSIF_NEWFILE位
保存IUnknown指針到punkRel
設置rect到當前窗口尺寸
退出消息循環
重要的是你不須要毀壞窗口,避免閃爍和生硬地改變用戶界面。你所返回的FVSHOWINFO結構被不變地傳遞給新文件觀察器。quikview.exe喚醒這個新觀察器(在此例中是處理TXT文件的),並調用它的ShowInitialize()方法來準備顯示。注意,此時咱們仍然有同一個位圖在屏幕上,甚至是徹底不一樣的模塊在這個封裝下工做。在TXT快速觀察器完成裝入和文字變換以後,quikview.exe調用Show()方法傳遞FVSHOWINFO結構,這是由BMP快速觀察器Show()方法返回的結構。這個結構包含了窗口應該佔有的精確區域,打開文件的名字,和前一個(仍然可視)快速觀察器的IUnknown指針。Show()能夠徹底覆蓋地顯示它的窗口。在這一點上,老窗口仍然在新窗口背後,事實上Show()方法還有更多的任務要執行。若是它發現FVSIF_NEWFILE標誌被設置,它就必須得到FVSHOWINFO的punkRel,而且調用Release()來釋放老的觀察器。
釘住鏈接
咱們所涉及到的第三個接口是IFileViewerSite,它有兩個至關容易的方法:
! GetPinnedWindow()
! SetPinnedWindow()
快速觀察器窗口在‘Replace Window’按鈕選中時被釘住。這個狀態使管理器直接定向全部請求到新快速觀察器窗口。若是一個窗口被釘住,點擊關聯菜單就等價於拖拽文件到那個窗口。GetPinnedWindow()就返回當前釘住的窗口Handle(記住,能夠同時打開不少快速觀察器),而SetPinnedWindow()則移動這個屬性到新窗口。它們的原型是:
HRESULT GetPinnedWindow(HWND*);
HRESULT SetPinnedWindow(HWND);
釘住之後的操做邏輯能夠歸納以下:
SetPinnedWindow()老是失敗,若是另外一個窗口被釘住
你老是須要本身拔除當前釘住的窗口——這能夠經過調用SetPinnedWindow()並設置NULL變量來完成
爲了使你知道是否你的窗口應該開始釘住,FVSHOWINFO包含了FVSIF_PINNED標誌在dwFlags成員中。如此,釘住窗口最明智的方法是下面這兩行代碼:
SetPinnedWindow(NULL);
SetPinnedWindow(hwnd);
寫並註冊文件觀察器
寫文件觀察器不是一個簡單的任務,在MSDN資料庫中能夠找到參考答案。寫好了觀察器後,註冊它就是直接的了。假設你已經準備了一個.ext類型的文件觀察器,下面是註冊鍵的改變:
[HKEY_CLASSES_ROOT/QuickView/.EXT/<CLSID>]
@="EXT File Viewer"
這裏的<CLSID>應該改成實際的CLSID,同時不要忘了註冊服務器,就象任何其它COM服務器註冊那樣。若是使用ATL創建對象,則添加下面行到RGS腳本文件:
{
QuickView
{
.ext
{
<CLSID> = s 'description'
}
}
}
小結
這一章極詳細地討論了Shell擴展技術。咱們檢測了它與探測器的集成,它們背後的邏輯,以及它們的實現。咱們還開發了幾個示例來講明各類類型的Shell擴展行爲。特別是,咱們察看了:
怎樣添加定製屬性頁到屬性對話框
怎樣添加定製菜單項到文檔關聯菜單
怎樣添加定製菜單項到系統‘查找’菜單
怎樣爲必定類型的每個文檔繪製客戶圖標
怎樣監視系統中任何文件夾的變化
怎樣處理Shell中文件的拖拽
咱們還給出了關於拷貝數據到剪裁板的實用技術,以及拖拽處理技術。最後介紹了文件觀察器,並集中討論了編程方面應注意的問題。