好久沒有給VC知識庫發稿了,實在不好意思,由於前段時間實在太忙所以一直沒有時間閒下心來寫點東西,期間也有不少朋友給我來信討論問題,我很感謝大家對我的支持,我歡迎大家繼續來信,共同交流,共同進步!這次我想和大家一起討論一下 Windows 的 Shell 擴展編程,首先在閱讀以下內容之前我還是推薦大家看一下《COM技術內幕》這本大作,不過即使您沒有有關的基礎知識其實也是無所謂的,因為以下講解是傻瓜式講解。
開發環境
Windows Professional 2000
Microsoft Visual C++ 6.0 + ATL3.0
參考文獻
COM技術內幕
ATL應用與開發指南(第二版)
Windows外殼擴展
Windows外殼擴展的英文名稱為:Windows Shell Extension。Windows外殼擴展是一類特殊的COM對象,在這類COM對象中用戶可以加入自己的特殊功能,而Windows外殼擴展最終都會被Windows Explorer所引用。舉個最簡單的例子,比如 WinRar 應用程序,如果你安裝完 WinRar 後,它會在你的右鍵菜單中加入很多快捷菜單,如 圖1.1 所示:
圖1.1
而上圖卻僅僅是外殼擴展編程中一種:"Context Menu Handler"。難道外殼擴展也分類嗎?是的,但是不多,並且它們的實現大都一致,總體來說有如下幾種分類:
表(一) 處理器類型 何時觸發 所做處理 Context menu 處理器 當用戶鼠標右擊文件或文件夾時觸發。但是在Shell V4.71+中,用戶在文件夾目錄的空白處點擊鼠標右鍵也會觸發該事件。 加入上下文菜單項。 Property sheet 處理器 當用戶鼠標右擊文件,選擇文件"屬性"菜單彈出文件屬性對話框時觸發。 加入用戶自定義屬性頁。 Drag and drop 處理器 當用戶在文件夾或桌面中用鼠標右鍵Drag/Drop文件或文件夾時觸發。 加入上下文菜單項。 Drop處理器 當某一數據對象被Drag Over/Dropped Into某一文件時觸發。 加入任何用戶自定義動作。 QueryInfo 處理器(Shell V4.71+) 當用戶鼠標滑過某一個文件或某一Shell對象時觸發。 加入用戶自定義提示信息(ToolTips)。
也許有人會問我實現它們困難嗎?答案是:比較簡單。實現它是不是必須得去看那些枯燥乏味的ATL模板類,或者生硬死板的 MFC 宏定義呢?答案是否定的。也許以上的問題阻礙了大多數COM初學者的學習欲望,其實我剛接觸ATL時多的是迷惘,常常抱怨 ATL 的知識太深奧,MFC的構架太生硬,一般我是不太喜歡用#define來定義程序的全部(請參閱 effective C++)。言歸正傳,我們再回到今天的話題上來,那麼為實現 圖1.1 所示功能可以通過哪些途徑呢?答案有二,第一:注冊表編程。第二:Shell Extension COM編程。通過注冊表方式實現其實十分簡單,請參閱 COM 組件注冊表實現,在這裡本文不做重復介紹,再者也不是本文的主題所在。在以下的內容中我會以第一類 Shell 擴展編程---" Context Menu 處理器" 為例來講解 Handler 的實現過程。
組件功能
該組件實現的功能為:當用戶在Explorer中鼠標右擊DLL類型文件時,在彈出的上下文菜單中注冊我們自己的菜單項,如圖1.2 所示:
圖1.2
"Register Component"和"UnRegister Component"菜單項既是我們自己的菜單項。並且這兩個菜單項分別完成進程內組件(DLL)的注冊和反注冊,菜單項的功能倒很簡單,只是簡單地執行了 Windows 的 Regsvr32.exe而已,但是我們已經感覺到它給我們帶來的實用和方便,難道你不覺得 "Over and Over" 手工輸入 "Regsvr32 xxx.dll" 或者 "Regsvr32 /u xxx.dll" 很乏味嗎……。
編寫組件
建立工程: 打開VC++,新建一個"ATL Com AppWizard"模板工程,工程名稱為:SimpleExt。
圖 1.3
Shell擴展實例均為進程內組件,它們均以動態庫的形式存在,所以在接下來的向導中我們用默認設置:"Dynamic Link Library(DLL)",然後點擊"完成"。如 下圖所示:
圖 1.4
此時我們已經擁有了一個沒有實現任何功能的進程內 COM 組件,為什麼說"沒有實現任何功能"呢?那是因為我們沒有實現任何接口,再者在我們的DLL中也沒有任何可供外部使用的接口。
如果我們的組件不繼承其他外部已有接口,那麼這樣的COM組件實現起來則非常簡單,它和編寫普通類代碼沒有任何不一樣的地方,只需要使用 ATL 接口的 Method 和Property 增/刪向導即可實現。
顯然我們的組件要繼承 Shell 的擴展接口,並且還得實現所有繼承的 Shell 接口,所以我們就不能完全依賴 ATL 的"自動化"了,這裡需要我們自己寫代碼來實現該接口。首先我們通過 AT L向導新增一個簡單接口 SimpleShlExt,如下圖1.5,圖1.6 和 圖1.7 所示操作過程:
圖 1.5
圖 1.6
圖 1.7
然後一切默認即可,這樣ATL就為我們生成了一個組件框架,我們以下的討論都基於此框架。
添加代碼
圖1.8 組件類繼承關系
圖1.8 中紅色方框是我們自己要實現的 Shell 擴展接口,它不是向導自動生成代碼,需要我們手工輸入。
我們從該框架中可以獲得很多好處,首先通過 ATL 的模板類 CcomCoClass 我們就可以省去反復再三的 QueryInterface 接口的實現,而我們只需要綁定組件和接口的映射關系(如下圖1.9 所示)以及實現所繼承接口的全部虛函數即可,以及組件的注冊等它基本上都為我們做好了一切,好處大家就慢慢體會吧......。下面我們首先介紹繼承的各接口和其虛成員函數的作用,它們的聲明包含在<shlobj.h>頭文件中,首先頭文件你必須包含進來:
圖1.9 建立組件和接口的映射關系
圖1.9 紅色方框為 IShellExtInit 和 IContextMenu 接口和組件的接口映射關系,它不是向導自動生成代碼,需要我們手工輸入。
IShellExtInit接口:IShellExtInit 接口為 Shell 擴展編程必須要實現的接口。該接口主要用來初始化 Shell 擴展處理器(表一所列的處理器),它僅有一個虛成員函數Initialize,用戶所有的 Shell 擴展初始化動作都由該函數完成。該函數的原型如下: HRESULT Initialize(
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj,
HKEY hkeyProgID
);
在 Initialize 函數中,我們要做的事情就是獲取用戶鼠標右鍵點擊的文件名稱,但是有可能用戶選擇了多個文件,這裡為了簡單起見我們僅獲取文件列表中的第一個文件。在這裡我們得補充一點內容:當用戶在一個擁有 WS_EX_ACCEPTFILES 風格的窗體中Drag/Drop 文件時這些文件名會以同一種格式存儲,而且文件完整路徑的獲取也都以DragQueryFile API函數來實現。但是 DragQueryFile 需要傳入一個 HDROP 句柄,該句柄即為 Drag/Drop 文件名稱列表數據句柄(開始存放數據的內存區域首指針)。而 HDROP 句柄的可以通過接口 " DATAOBJECT lpdobj" 的成員函數" GetData" 來獲取。以下為獲取第一個 Drag/Drop 文件的完整文件路徑的具體代碼:
//數據存儲格式 FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; //數據存儲內存句柄(常用於IDataObject和IAdviseSink接口的數據傳輸操作) STGMEDIUM stg = { TYMED_HGLOBAL }; if(FAILED(pDataObj->GetData(&fmt, &stg))) { //如果獲取數據內存句柄失敗則返回E_INVALIDARG, //返回E_INVALIDARG則Explorer不會再調用我們的Shell擴展接口 return E_INVALIDARG; } //獲取實際數據內存句柄 HDROP hDrop = (HDROP)GlobalLock(stg.hGlobal); if(NULL==hDrop) { //在COM程序中養成良好的檢錯習慣是很重要的!!! return E_INVALIDARG; } //獲取用戶Drag/Drop的文件數目 int nDropCount = ::DragQueryFile((HDROP)stg.hGlobal, 0xFFFFFFFF, NULL, 0); //本示例程序僅獲取第一個Drag/Drop文件完整路徑 //以下注釋代碼為獲取所有文件完整路徑的實現代碼: //for(int i = 0; i < nDropCount; ++i){ //循環獲取每個Drag/Drop文件的完整文件名 // ::DragQueryFile((HDROP)stg.hGlobal, i, m_pzDropFile, MAX_PATH); //} //如果用戶Drag/Drop的文件數目不為一個則不予處理 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 接口已經完全實現,從此我們也可以看出進程內組件編程的一些特點,大體總結如下:"新建自己的接口,然後繼承某些接口,最後一一實現這些接口的所有虛成員函數或 加入自己的成員函數,最後就是組件的注冊"。 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 實現外還得需要注冊表的特殊注冊才可以實現。其中 IContextMenu 接口有三個虛成員函數需要我們的組件來實現,其函數原型分別如下:
HRESULT QueryContextMenu( HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags );
注:在QueryContextMenu 成員函數中我們可以加入自己的菜單項,插入菜單項其實很簡單,我們可以通過 InsertMenu API 函數來實現,如下代碼所示:
::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 成員函數為 Explorer 提供了在狀態欄顯示菜單命令提示信息的方法。在這個方法中 "LPSTR pszName" 是我們要關注的參數,我們只要根據 "UINT uFlags" 參數來填充 "LPSTR pszName" 參數即可。在這裡可能會涉及到 ANSI 和 UNICODE 之間相互轉換的知識,不過在這裡我要提醒大家的是:在 COM 編程中盡可能使用兼容的 TCHAR 類型,同時對字符操作也盡量不要使用 C 類的 <string.h> 和<stdio.h> 等等函數庫,因為這樣會使您無法通過 "Win32 Release Mindependency " 或其他 UINCode/Release 版本的編譯過程。
HRESULT InvokeCommand( LPCMINVOKECOMMANDINFO pici );
InvokeCommand 函數實現最終菜單項命令的執行。在 "LPCMINVOKECOMMANDINFO pici" 參數中包含了當前用戶執行的菜單項ID和其他一些標志信息,如下代碼可獲取菜單項的ID:
//如果 nFlag 不為0則說明 pici->lpVerb 指向一個以''\0''結尾的字符串
int nFlag = HIWORD(pici->lpVerb);
//用戶當前點擊的菜單項ID
int nMnuId = LOWORD(lpici->lpVerb);
一旦獲取了菜單項ID那麼我們就可以根據不同的菜單項來執行相應的動作,如圖1.2 所示的 "Register Component" 和 "UnRegister Component" 菜單項所對應的 "注冊/反注冊進程內組件" 動作。
組件必要的宏定義部分
其實這一步十分簡單,本可以忽略,但是為了把過程講的更清楚一點我還是列了出來:
//聲明組件注冊所用的注冊表REG資源 //其中IDR_SIMPLESHLEXT為注冊表資源ID DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLESHLEXT) //AddRef和Release成員函數的實現 DECLARE_PROTECT_FINAL_CONSTRUCT() //組件接口映射部分,該部分映射主要是告訴QueryInterface能返回哪些接口給外部 BEGIN_COM_MAP(CSimpleShlExt) COM_INTERFACE_ENTRY(ISimpleShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit) //IShellExtInit接口 COM_INTERFACE_ENTRY(IContextMenu) //IContextMenu接口 END_COM_MAP()
組件注冊
首先要在系統文件類型".DLL"下注冊上下文菜單處理器
創建注冊表項HKCR\dllfile\ShellEx\ContextMenuHandlers\SimpleShlExt,
並設置其默認值為我們類的GUID值即可。
設置訪問許可權(適應於WinNT構架的操作系統)
如果您不設置該選項則只有 Administrator 權限的用戶才可以使用該 Shell 擴展
在 HKCM\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved 注冊表項下創建鍵值:
鍵名為:類的 GUID
鍵值為:有關類的描述信息(任意字符串,無特殊要求)。
組件注冊的程序內部實現
DLL文件上下文菜單的實現通過 REG 注冊表資源文件實現,描述層次結構如下:
HKCR { NoRemove dllfile { NoRemove ShellEx { NoRemove ContextMenuHandlers { //類的GUID字符串 ForceRemove SimpleShlExt = s''{7C108295-19DE-4093-A9F8-ACC5E031E27A}'' } } } }
訪問許可權的注冊則在DllRegisterServer DLL輸出函數中完成。
其實現只需要使用注冊表的Win32 API函數即可;相應的反注冊組件時則應在 DllUnregisterServer中刪除相應的注冊表鍵值即可。
好了,"Context Menu 處理器" 的實現到此完畢,還是老規矩如有問題請直接來信,我期待大家的來信,同時在以後的時間裡我會繼續討論Shell擴展編程,並會對我以前的文章大家所存在的疑點做出合適的回答,再次感謝大家的支持和鼓勵。
本文配套源碼