當(dāng)前位置:首頁 > 芯聞號(hào) > 充電吧
[導(dǎo)讀]這次我想和大家一起討論一下 Windows 的 Shell 擴(kuò)展編程,首先在閱讀以下內(nèi)容之前我還是推薦大家看一下《COM技術(shù)內(nèi)幕》這本大作,不過即使您沒有有關(guān)的基礎(chǔ)知識(shí)其實(shí)也是無所謂的,因?yàn)橐韵轮v解是


這次我想和大家一起討論一下 Windows 的 Shell 擴(kuò)展編程,首先在閱讀以下內(nèi)容之前我還是推薦大家看一下《COM技術(shù)內(nèi)幕》這本大作,不過即使您沒有有關(guān)的基礎(chǔ)知識(shí)其實(shí)也是無所謂的,因?yàn)橐韵轮v解是傻瓜式講解。

開發(fā)環(huán)境

Windows Professional 2000Microsoft Visual C++ 6.0 + ATL3.0

參考文獻(xiàn)

COM技術(shù)內(nèi)幕ATL應(yīng)用與開發(fā)指南(第二版)

Windows外殼擴(kuò)展
??? Windows外殼擴(kuò)展的英文名稱為:Windows Shell Extension。Windows外殼擴(kuò)展是一類特殊的COM對(duì)象,在這類COM對(duì)象中用戶可以加入自己的特殊功能,而Windows外殼擴(kuò)展最終都會(huì) 被Windows Explorer所引用。舉個(gè)最簡(jiǎn)單的例子,比如 WinRar 應(yīng)用程序,如果你安裝完 WinRar 后,它會(huì)在你的右鍵菜單中加入很多快捷菜單,如 圖1.1 所示:

圖1.1

而上圖卻僅僅是外殼擴(kuò)展編程中一種:"Context Menu Handler"。難道外殼擴(kuò)展也分類嗎?是的,但是不多,并且它們的實(shí)現(xiàn)大都一致,總體來說有如下幾種分類:

表(一)

處理器類型 何時(shí)觸發(fā) 所做處理 Context menu 處理器 當(dāng)用戶鼠標(biāo)右擊文件或文件夾時(shí)觸發(fā)。但是在Shell V4.71+中,用戶在文件夾目錄的空白處點(diǎn)擊鼠標(biāo)右鍵也會(huì)觸發(fā)該事件。 加入上下文菜單項(xiàng)。 Property sheet 處理器 當(dāng)用戶鼠標(biāo)右擊文件,選擇文件"屬性"菜單彈出文件屬性對(duì)話框時(shí)觸發(fā)。 加入用戶自定義屬性頁。 Drag and drop 處理器 當(dāng)用戶在文件夾或桌面中用鼠標(biāo)右鍵Drag/Drop文件或文件夾時(shí)觸發(fā)。 加入上下文菜單項(xiàng)。 Drop處理器 當(dāng)某一數(shù)據(jù)對(duì)象被Drag Over/Dropped Into某一文件時(shí)觸發(fā)。 加入任何用戶自定義動(dòng)作。 QueryInfo 處理器(Shell V4.71+) 當(dāng)用戶鼠標(biāo)滑過某一個(gè)文件或某一Shell對(duì)象時(shí)觸發(fā)。 加入用戶自定義提示信息(ToolTips)。

?

??? 也許有人會(huì)問我實(shí)現(xiàn)它們困難嗎?答案是:比較簡(jiǎn)單。實(shí)現(xiàn)它是不是必須得去看那些枯燥乏味的ATL模板類,或者生硬死板的 MFC 宏定義呢?答案是否定的。也許以上的問題阻礙了大多數(shù)COM初學(xué)者的學(xué)習(xí)欲望,其實(shí)我剛接觸ATL時(shí)多的是迷惘,常常抱怨 ATL 的知識(shí)太深?yuàn)W,MFC的構(gòu)架太生硬,一般我是不太喜歡用#define來定義程序的全部(請(qǐng)參閱 effective C++)。言歸正傳,我們?cè)倩氐浇裉斓脑掝}上來,那么為實(shí)現(xiàn) 圖1.1 所示功能可以通過哪些途徑呢?答案有二,第一:注冊(cè)表編程。第二:Shell Extension COM編程。通過注冊(cè)表方式實(shí)現(xiàn)其實(shí)十分簡(jiǎn)單,請(qǐng)參閱 COM?組件注冊(cè)表實(shí)現(xiàn),在這里本文不做重復(fù)介紹,再者也不是本文的主題所在。在以下的內(nèi)容中我會(huì)以第一類 Shell 擴(kuò)展編程---" Context Menu 處理器" 為例來講解 Handler 的實(shí)現(xiàn)過程。

組件功能
??? 該組件實(shí)現(xiàn)的功能為:當(dāng)用戶在Explorer中鼠標(biāo)右擊DLL類型文件時(shí),在彈出的上下文菜單中注冊(cè)我們自己的菜單項(xiàng),如圖1.2 所示:

圖1.2

"Register Component"和"UnRegister Component"菜單項(xiàng)既是我們自己的菜單項(xiàng)。并且這兩個(gè)菜單項(xiàng)分別完成進(jìn)程內(nèi)組件(DLL) 的注冊(cè)和反注冊(cè),菜單項(xiàng)的功能倒很簡(jiǎn)單,只是簡(jiǎn)單地執(zhí)行了 Windows 的 Regsvr32.exe而已,但是我們已經(jīng)感覺到它給我們帶來的實(shí)用和方便,難道你不覺得 "Over and Over" 手工輸入 "Regsvr32 xxx.dll" 或者 "Regsvr32 /u xxx.dll" 很乏味嗎……。

編寫組件? 

建立工程: 打開VC++,新建一個(gè)"ATL Com AppWizard"模板工程,工程名稱為:SimpleExt。

圖 1.3

Shell擴(kuò)展實(shí)例均為進(jìn)程內(nèi)組件,它們均以動(dòng)態(tài)庫的形式存在,所以在接下來的向?qū)е形覀冇媚J(rèn)設(shè)置:"Dynamic Link Library(DLL)",然后點(diǎn)擊"完成"。如 下圖所示:

圖 1.4

此時(shí)我們已經(jīng)擁有了一個(gè)沒有實(shí)現(xiàn)任何功能的進(jìn)程內(nèi) COM?組件,為什么說"沒有實(shí)現(xiàn)任何功能"呢?那是因?yàn)槲覀儧]有實(shí)現(xiàn)任何接口,再者在我們的DLL中也沒有任何可供外部使用的接口。
??? 如果我們的組件不繼承其他外部已有接口,那么這樣的COM組件實(shí)現(xiàn)起來則非常簡(jiǎn)單,它和編寫普通類代碼沒有任何不一樣的地方,只需要使用 ATL 接口的 Method 和Property 增/刪向?qū)Ъ纯蓪?shí)現(xiàn)。
??? 顯然我們的組件要 繼承 Shell 的擴(kuò)展接口,并且還得實(shí)現(xiàn)所有繼承的 Shell 接口,所以我們就不能完全依賴 ATL 的"自動(dòng)化"了,這里需要我們自己寫代碼來實(shí)現(xiàn)該接口。首先我們通過 AT L向?qū)略鲆粋€(gè)簡(jiǎn)單接口 SimpleShlExt,如下圖1.5,圖1.6 和 圖1.7 所示操作過程:

圖 1.5


圖 1.6

圖 1.7

然后一切默認(rèn)即可,這樣ATL就為我們生成了一個(gè)組件框架,我們以下的討論都基于此框架。

2.添加代碼

圖1.8?組件類繼承關(guān)系

圖1.8 中紅色方框是我們自己要實(shí)現(xiàn)的 Shell 擴(kuò)展接口,它不是向?qū)ё詣?dòng)生成代碼,需要我們手工輸入。

??? 我們從該框架中可以獲得很多好處,首先通過 ATL 的模板類 CcomCoClass 我們就可以省去反復(fù)再三的 QueryInterface 接口的實(shí)現(xiàn),而我們只需要綁定組件和接口的映射關(guān)系(如下圖1.9 所示)以及實(shí)現(xiàn)所繼承接口的全部虛函數(shù)即可,以及組件的注冊(cè)等它基本上都為我們做好了一切,好處大家就慢慢體會(huì)吧......。下面我們首先介紹繼承的各接口和其虛成員函數(shù)的作用,它們的聲明包含在頭文件中,首先頭文件你必須包含進(jìn)來:

圖1.9 建立組件和接口的映射關(guān)系

圖1.9 紅色方框?yàn)?IShellExtInit 和 IContextMenu 接口和組件的接口映射關(guān)系,它不是向?qū)ё詣?dòng)生成代碼,需要我們手工輸入。

IShellExtInit接口:IShellExtInit 接口為 Shell 擴(kuò)展編程必須要實(shí)現(xiàn)的接口。該接口主要用來初始化 Shell 擴(kuò)展處理器(表一所列的處理器),它僅有一個(gè)虛成員函數(shù)Initialize,用戶所有的 Shell 擴(kuò)展初始化動(dòng)作都由該函數(shù)完成。該函數(shù)的原型如下:

?

HRESULT Initialize(
??? LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj,
HKEY hkeyProgID
);

?

??? 在 Initialize 函數(shù)中,我們要做的事情就是獲取用戶鼠標(biāo)右鍵點(diǎn)擊的文件名稱,但是有可能用戶選擇了多個(gè)文件,這里為了簡(jiǎn)單起見我們僅獲取文件列表中的第一個(gè)文件。在這里 我們得補(bǔ)充一點(diǎn)內(nèi)容:當(dāng)用戶在一個(gè)擁有 WS_EX_ACCEPTFILES 風(fēng)格的窗體中Drag/Drop 文件時(shí)這些文件名會(huì)以同一種格式存儲(chǔ),而且文件完整路徑的獲取也都以DragQueryFile API函數(shù)來實(shí)現(xiàn)。但是 DragQueryFile 需要傳入一個(gè) HDROP 句柄,該句柄即為 Drag/Drop 文件名稱列表數(shù)據(jù)句柄(開始存放數(shù)據(jù)的內(nèi)存區(qū)域首指針)。而 HDROP 句柄的可以通過接口 " DATAOBJECT lpdobj" 的成員函數(shù)" GetData" 來獲取。以下為獲取第一個(gè) Drag/Drop 文件的完整文件路徑的具體代碼:

?

//數(shù)據(jù)存儲(chǔ)格式
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };

//數(shù)據(jù)存儲(chǔ)內(nèi)存句柄(常用于IDataObject和IAdviseSink接口的數(shù)據(jù)傳輸操作)
STGMEDIUM stg = { TYMED_HGLOBAL };

if(FAILED(pDataObj->GetData(&fmt, &stg)))
{
???? //如果獲取數(shù)據(jù)內(nèi)存句柄失敗則返回E_INVALIDARG,
???? //返回E_INVALIDARG則Explorer不會(huì)再調(diào)用我們的Shell擴(kuò)展接口
???? return E_INVALIDARG;
}

//獲取實(shí)際數(shù)據(jù)內(nèi)存句柄
HDROP hDrop = (HDROP)GlobalLock(stg.hGlobal);
if(NULL==hDrop)
{
???? //在COM程序中養(yǎng)成良好的檢錯(cuò)習(xí)慣是很重要的?。。?br />???? return E_INVALIDARG;
}
//獲取用戶Drag/Drop的文件數(shù)目
int nDropCount = ::DragQueryFile((HDROP)stg.hGlobal,?
0xFFFFFFFF, NULL, 0);

//本示例程序僅獲取第一個(gè)Drag/Drop文件完整路徑
//以下注釋代碼為獲取所有文件完整路徑的實(shí)現(xiàn)代碼:
//for(int i = 0; i < nDropCount; ++i){
???? //循環(huán)獲取每個(gè)Drag/Drop文件的完整文件名
???? // ::DragQueryFile((HDROP)stg.hGlobal, i, m_pzDropFile, MAX_PATH);
//}

//如果用戶Drag/Drop的文件數(shù)目不為一個(gè)則不予處理
if(1==nDropCount)
{
???? //pzDropFile為組件類內(nèi)部的private變量
???? //它用來保存用戶Drag/Drop的文件完整文件名
???? memset(m_pzDropFile, 0x0, MAX_PATH*sizeof(TCHAR));
???? ::DragQueryFile((HDROP)stg.hGlobal, 0, m_pzDropFile, MAX_PATH);
}

//釋放內(nèi)存句柄
::ReleaseStgMedium(&mdmSTG);

?

至此 IShellExtInit 接口已經(jīng)完全實(shí)現(xiàn),從此我們也可以看出進(jìn)程內(nèi)組件編程的一些特點(diǎn),大體總結(jié)如下:"新建自己的接口,然后繼承某些接口,最后一一實(shí)現(xiàn)這些接口的所有虛成員函數(shù)或 加入自己的成員函數(shù),最后就是組件的注冊(cè)"?!? IContextMenu 接口:該接口和 "Context Menu 處理器" 一一對(duì)應(yīng),說到此我們也順便說一下 Shell 擴(kuò)展接口編程中和(表一)中所列處理器各自對(duì)應(yīng)的COM接口:

(表二)

處理器類型 COM接口 Context menu 處理器 IContextMenu Property sheet 處理器 IShellPropSheetExt Drag and drop 處理器 IContextMenu Drop 處理器 IDropTarget QueryInfo 處理器(Shell V4.71+) IQueryInfo

?

其中 "Drag and drop 處理器" 的除了 COM 接口 IContextMenu 實(shí)現(xiàn)外還得需要注冊(cè)表的特殊注冊(cè)才可以實(shí)現(xiàn)。其中 IContextMenu 接口有三個(gè)虛成員函數(shù)需要我們的組件來實(shí)現(xiàn),其函數(shù)原型分別如下:

?

HRESULT QueryContextMenu(
??? HMENU hmenu,
??? UINT indexMenu,
??? UINT idCmdFirst,
??? UINT idCmdLast,
??? UINT uFlags
);

?

注:在QueryContextMenu 成員函數(shù)中我們可以加入自己的菜單項(xiàng),插入菜單項(xiàng)其實(shí)很簡(jiǎn)單,我們可以通過 InsertMenu API 函數(shù)來實(shí)現(xiàn),如下代碼所示:

?

::InsertMenu(hmenu, indexMenu, MF_STRING | MF_BYPOSITION,?
idCmdFirst, IDM_REG_MNU_TXT);

?

QueryContextMenu 的處理過程十分簡(jiǎn)單,在這里無須多說。

?

HRESULT GetCommandString(
?? UINT idCmd,
?? UINT uFlags,
?? UINT *pwReserved,
??? LPSTR pszName,
??? UINT cchMax
);

?

注:GetCommandString 成員函數(shù)為 Explorer 提供了在狀態(tài)欄顯示菜單命令提示信息的方法。在這個(gè)方法中 "LPSTR pszName" 是我們要關(guān)注的參數(shù),我們只要根據(jù) "UINT uFlags" 參數(shù)來填充 "LPSTR pszName" 參數(shù)即可。在這里可能會(huì)涉及到 ANSI 和 UNICODE 之間相互轉(zhuǎn)換的知識(shí),不過在這里我要提醒大家的是:在 COM?編程中盡可能使用兼容的 TCHAR 類型,同時(shí)對(duì)字符操作也盡量不要使用 C 類的?和?等等函數(shù)庫,因?yàn)檫@樣會(huì)使您無法通過 "Win32 Release Mindependency " 或其他 UINCode/Release 版本的編譯過程。

?

HRESULT InvokeCommand(
??? LPCMINVOKECOMMANDINFO pici
);

?

InvokeCommand 函數(shù)實(shí)現(xiàn)最終菜單項(xiàng)命令的執(zhí)行。在 "LPCMINVOKECOMMANDINFO pici" 參數(shù)中包含了當(dāng)前用戶執(zhí)行的菜單項(xiàng)ID和其他一些標(biāo)志信息,如下代碼可獲取菜單項(xiàng)的ID:

?

//如果 nFlag 不為0則說明 pici->lpVerb 指向一個(gè)以''