這次我想和大家一起討論一下 Windows 的 Shell 擴展編程,首先在閱讀以下內容之前我還是推薦大家看一下《COM技術內幕》這本大作,不過即使您沒有有關的基礎知識其實也是無所謂的,因為以下講解是傻瓜式講解。
開發(fā)環(huán)境
Windows Professional 2000Microsoft Visual C++ 6.0 + ATL3.0
參考文獻
COM技術內幕ATL應用與開發(fā)指南(第二版)
Windows外殼擴展
??? Windows外殼擴展的英文名稱為:Windows Shell Extension。Windows外殼擴展是一類特殊的COM對象,在這類COM對象中用戶可以加入自己的特殊功能,而Windows外殼擴展最終都會 被Windows Explorer所引用。舉個最簡單的例子,比如 WinRar 應用程序,如果你安裝完
WinRar 后,它會在你的右鍵菜單中加入很多快捷菜單,如 圖1.1 所示:
圖1.1
而上圖卻僅僅是外殼擴展編程中一種:"Context Menu Handler"。難道外殼擴展也分類嗎?是的,但是不多,并且它們的實現(xiàn)大都一致,總體來說有如下幾種分類:
表(一)
處理器類型 何時觸發(fā) 所做處理 Context menu 處理器 當用戶鼠標右擊文件或文件夾時觸發(fā)。但是在Shell V4.71+中,用戶在文件夾目錄的空白處點擊鼠標右鍵也會觸發(fā)該事件。 加入上下文菜單項。 Property sheet 處理器 當用戶鼠標右擊文件,選擇文件"屬性"菜單彈出文件屬性對話框時觸發(fā)。 加入用戶自定義屬性頁。 Drag and drop 處理器 當用戶在文件夾或桌面中用鼠標右鍵Drag/Drop文件或文件夾時觸發(fā)。 加入上下文菜單項。 Drop處理器 當某一數(shù)據對象被Drag Over/Dropped Into某一文件時觸發(fā)。 加入任何用戶自定義動作。 QueryInfo 處理器(Shell V4.71+) 當用戶鼠標滑過某一個文件或某一Shell對象時觸發(fā)。 加入用戶自定義提示信息(ToolTips)。
?
??? 也許有人會問我實現(xiàn)它們困難嗎?答案是:比較簡單。實現(xiàn)它是不是必須得去看那些枯燥乏味的ATL模板類,或者生硬死板的 MFC 宏定義呢?答案是否定的。也許以上的問題阻礙了大多數(shù)COM初學者的學習欲望,其實我剛接觸ATL時多的是迷惘,常常抱怨 ATL 的知識太深奧,MFC的構架太生硬,一般我是不太喜歡用#define來定義程序的全部(請參閱
effective C++)。言歸正傳,我們再回到今天的話題上來,那么為實現(xiàn) 圖1.1 所示功能可以通過哪些途徑呢?答案有二,第一:注冊表編程。第二:Shell Extension COM編程。通過注冊表方式實現(xiàn)其實十分簡單,請參閱
COM?組件注冊表實現(xiàn),在這里本文不做重復介紹,再者也不是本文的主題所在。在以下的內容中我會以第一類 Shell 擴展編程---" Context Menu
處理器" 為例來講解 Handler 的實現(xiàn)過程。
組件功能
??? 該組件實現(xiàn)的功能為:當用戶在Explorer中鼠標右擊DLL類型文件時,在彈出的上下文菜單中注冊我們自己的菜單項,如圖1.2 所示:
圖1.2
"Register Component"和"UnRegister Component"菜單項既是我們自己的菜單項。并且這兩個菜單項分別完成進程內組件(DLL) 的注冊和反注冊,菜單項的功能倒很簡單,只是簡單地執(zhí)行了 Windows 的 Regsvr32.exe而已,但是我們已經感覺到它給我們帶來的實用和方便,難道你不覺得 "Over
and Over" 手工輸入 "Regsvr32 xxx.dll" 或者 "Regsvr32 /u xxx.dll" 很乏味嗎……。
編寫組件?
建立工程: 打開VC++,新建一個"ATL Com AppWizard"模板工程,工程名稱為:SimpleExt。
圖 1.3
Shell擴展實例均為進程內組件,它們均以動態(tài)庫的形式存在,所以在接下來的向導中我們用默認設置:"Dynamic Link Library(DLL)",然后點擊"完成"。如 下圖所示:
圖 1.4
此時我們已經擁有了一個沒有實現(xiàn)任何功能的進程內 COM?組件,為什么說"沒有實現(xiàn)任何功能"呢?那是因為我們沒有實現(xiàn)任何接口,再者在我們的DLL中也沒有任何可供外部使用的接口。
??? 如果我們的組件不繼承其他外部已有接口,那么這樣的COM組件實現(xiàn)起來則非常簡單,它和編寫普通類代碼沒有任何不一樣的地方,只需要使用 ATL 接口的
Method 和Property 增/刪向導即可實現(xiàn)。
??? 顯然我們的組件要 繼承 Shell 的擴展接口,并且還得實現(xiàn)所有繼承的 Shell 接口,所以我們就不能完全依賴 ATL 的"自動化"了,這里需要我們自己寫代碼來實現(xiàn)該接口。首先我們通過 AT L向導新增一個簡單接口 SimpleShlExt,如下圖1.5,圖1.6 和 圖1.7 所示操作過程:
圖 1.5
圖 1.6
圖 1.7
然后一切默認即可,這樣ATL就為我們生成了一個組件框架,我們以下的討論都基于此框架。
2.添加代碼
圖1.8?組件類繼承關系
圖1.8 中紅色方框是我們自己要實現(xiàn)的 Shell 擴展接口,它不是向導自動生成代碼,需要我們手工輸入。
??? 我們從該框架中可以獲得很多好處,首先通過 ATL 的模板類 CcomCoClass 我們就可以省去反復再三的 QueryInterface 接口的實現(xiàn),而我們只需要綁定組件和接口的映射關系(如下圖1.9 所示)以及實現(xiàn)所繼承接口的全部虛函數(shù)即可,以及組件的注冊等它基本上都為我們做好了一切,好處大家就慢慢體會吧......。下面我們首先介紹繼承的各接口和其虛成員函數(shù)的作用,它們的聲明包含在頭文件中,首先頭文件你必須包含進來:
圖1.9 建立組件和接口的映射關系
圖1.9 紅色方框為 IShellExtInit 和 IContextMenu 接口和組件的接口映射關系,它不是向導自動生成代碼,需要我們手工輸入。
IShellExtInit接口:IShellExtInit 接口為 Shell 擴展編程必須要實現(xiàn)的接口。該接口主要用來初始化 Shell 擴展處理器(表一所列的處理器),它僅有一個虛成員函數(shù)Initialize,用戶所有的
Shell 擴展初始化動作都由該函數(shù)完成。該函數(shù)的原型如下:
?
HRESULT Initialize(
??? LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj,
HKEY hkeyProgID
);
?
??? 在 Initialize 函數(shù)中,我們要做的事情就是獲取用戶鼠標右鍵點擊的文件名稱,但是有可能用戶選擇了多個文件,這里為了簡單起見我們僅獲取文件列表中的第一個文件。在這里 我們得補充一點內容:當用戶在一個擁有 WS_EX_ACCEPTFILES 風格的窗體中Drag/Drop 文件時這些文件名會以同一種格式存儲,而且文件完整路徑的獲取也都以DragQueryFile API函數(shù)來實現(xiàn)。但是 DragQueryFile 需要傳入一個 HDROP 句柄,該句柄即為 Drag/Drop 文件名稱列表數(shù)據句柄(開始存放數(shù)據的內存區(qū)域首指針)。而 HDROP 句柄的可以通過接口 " DATAOBJECT lpdobj" 的成員函數(shù)" GetData" 來獲取。以下為獲取第一個 Drag/Drop 文件的完整文件路徑的具體代碼:
?
//數(shù)據存儲格式
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
//數(shù)據存儲內存句柄(常用于IDataObject和IAdviseSink接口的數(shù)據傳輸操作)
STGMEDIUM stg = { TYMED_HGLOBAL };
if(FAILED(pDataObj->GetData(&fmt, &stg)))
{
???? //如果獲取數(shù)據內存句柄失敗則返回E_INVALIDARG,
???? //返回E_INVALIDARG則Explorer不會再調用我們的Shell擴展接口
???? return E_INVALIDARG;
}
//獲取實際數(shù)據內存句柄
HDROP hDrop = (HDROP)GlobalLock(stg.hGlobal);
if(NULL==hDrop)
{
???? //在COM程序中養(yǎng)成良好的檢錯習慣是很重要的?。?!
???? return E_INVALIDARG;
}
//獲取用戶Drag/Drop的文件數(shù)目
int nDropCount = ::DragQueryFile((HDROP)stg.hGlobal,?
0xFFFFFFFF, NULL, 0);
//本示例程序僅獲取第一個Drag/Drop文件完整路徑
//以下注釋代碼為獲取所有文件完整路徑的實現(xiàn)代碼:
//for(int i = 0; i < nDropCount; ++i){
???? //循環(huán)獲取每個Drag/Drop文件的完整文件名
???? // ::DragQueryFile((HDROP)stg.hGlobal, i, m_pzDropFile, MAX_PATH);
//}
//如果用戶Drag/Drop的文件數(shù)目不為一個則不予處理
if(1==nDropCount)
{
???? //pzDropFile為組件類內部的private變量
???? //它用來保存用戶Drag/Drop的文件完整文件名
???? memset(m_pzDropFile, 0x0, MAX_PATH*sizeof(TCHAR));
???? ::DragQueryFile((HDROP)stg.hGlobal, 0, m_pzDropFile, MAX_PATH);
}
//釋放內存句柄
::ReleaseStgMedium(&mdmSTG);
?
至此 IShellExtInit 接口已經完全實現(xiàn),從此我們也可以看出進程內組件編程的一些特點,大體總結如下:"新建自己的接口,然后繼承某些接口,最后一一實現(xiàn)這些接口的所有虛成員函數(shù)或
加入自己的成員函數(shù),最后就是組件的注冊"?!? IContextMenu 接口:該接口和 "Context Menu 處理器" 一一對應,說到此我們也順便說一下 Shell 擴展接口編程中和(表一)中所列處理器各自對應的COM接口:
(表二)
處理器類型 COM接口 Context menu 處理器 IContextMenu Property sheet 處理器 IShellPropSheetExt Drag and drop 處理器 IContextMenu Drop 處理器 IDropTarget QueryInfo 處理器(Shell V4.71+) IQueryInfo
?
其中 "Drag and drop 處理器" 的除了 COM 接口 IContextMenu 實現(xiàn)外還得需要注冊表的特殊注冊才可以實現(xiàn)。其中 IContextMenu 接口有三個虛成員函數(shù)需要我們的組件來實現(xiàn),其函數(shù)原型分別如下:
?
HRESULT QueryContextMenu(
??? HMENU hmenu,
??? UINT indexMenu,
??? UINT idCmdFirst,
??? UINT idCmdLast,
??? UINT uFlags
);
?
注:在QueryContextMenu 成員函數(shù)中我們可以加入自己的菜單項,插入菜單項其實很簡單,我們可以通過 InsertMenu API 函數(shù)來實現(xiàn),如下代碼所示:
?
::InsertMenu(hmenu, indexMenu, MF_STRING | MF_BYPOSITION,?
idCmdFirst, IDM_REG_MNU_TXT);
?
QueryContextMenu 的處理過程十分簡單,在這里無須多說。
?
HRESULT GetCommandString(
?? UINT idCmd,
?? UINT uFlags,
?? UINT *pwReserved,
??? LPSTR pszName,
??? UINT cchMax
);
?
注:GetCommandString 成員函數(shù)為 Explorer 提供了在狀態(tài)欄顯示菜單命令提示信息的方法。在這個方法中 "LPSTR pszName" 是我們要關注的參數(shù),我們只要根據 "UINT uFlags" 參數(shù)來填充 "LPSTR pszName" 參數(shù)即可。在這里可能會涉及到 ANSI 和 UNICODE 之間相互轉換的知識,不過在這里我要提醒大家的是:在 COM?編程中盡可能使用兼容的 TCHAR 類型,同時對字符操作也盡量不要使用 C 類的?和?等等函數(shù)庫,因為這樣會使您無法通過 "Win32 Release Mindependency " 或其他 UINCode/Release 版本的編譯過程。
?
HRESULT InvokeCommand(
??? LPCMINVOKECOMMANDINFO pici
);
?
InvokeCommand 函數(shù)實現(xiàn)最終菜單項命令的執(zhí)行。在 "LPCMINVOKECOMMANDINFO pici" 參數(shù)中包含了當前用戶執(zhí)行的菜單項ID和其他一些標志信息,如下代碼可獲取菜單項的ID:
?
//如果 nFlag 不為0則說明 pici->lpVerb 指向一個以''