程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 由ATL想起的外殼擴展編程(一)

由ATL想起的外殼擴展編程(一)

編輯:關於VC++

好久沒有給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擴展編程,並會對我以前的文章大家所存在的疑點做出合適的回答,再次感謝大家的支持和鼓勵。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved