引用自:http://blog.163.com/yesaidu@126/blog/static/51819307200861853827582/
Part I: A step-by-step tutorial on writing shell extensions
第一節:Windows shell擴展初步:上下文菜單擴展
作者:Michael Dunn
譯者:yesaidu
源代碼下載:1 2
目錄
● README
● 系列緒言
● 第一部分緒言
● 從AppWizard開始
● 初始化接口
● 上下文菜單交互接口
○ 更改上下文菜單
○ 在狀態欄顯示拉線式(fly-by)幫助
○ 執行用戶選擇
○ 其他代碼細節
● 注冊Shell擴展
● 調試Shell擴展
● 所有的外觀
● 版權與許可
● 修訂歷史
README
我想,你在行動之前,或者你在本手冊的討論板發帖之前應該閱讀這份材料。
本手冊最初是用VC 6編寫的。現在,VC8都出來了,我感覺是時候對本手冊進行升級到VC7.1了。(通過VC7.1自動升級VC6項目,并不一定會完全地完成代碼轉換;因此,VC7.1用戶可能碰到這樣的現象,即在轉換、編譯示例代碼后,運行時可能沒有效果或出錯。)只要我仔細檢查并更新本手冊,本手冊將體現VC7.1的新特點。我將會提供VC7.1項目的源碼下載。
VC2005用戶要注意了:VC2005體驗版(Express edition)沒有一同發布ATL或MFC。既然本手冊用到了ATL,有時還使用了MFC,因此,你不能用VC2005體驗版來編譯示例代碼。
如果你正使用VC6,那么,你應該設法取得最新的平臺SDK。你可以使用WEB安裝版(web install version),或者下載CAB文件或者ISO鏡像包,安裝它們到本地。確認把SDK的INCLUDE和LIB目錄添加到了VC的搜索路徑中。你能在PSDK程序組中找到Visual Studio Registration目錄。這是一個好主意,無論你使用VC7,還是用VC8,你都能取得最新的PSDK頭文件和庫文件。

VC7用戶注意了:如果你沒有更新PSDK,必須改變默認的INCLUDE路徑。確信“VC++目錄”-“包含文件”列表的第一項是$(VCInstallDir)PlatformSDK\include
,它在($VCInstallDir)include
前面,如下圖:

由于一直沒有使用過VC 8,因此我不確定示例代碼在VC 8上是否可以通過編譯。只是希望,把VC7項目升級到VC8的自動轉換功能比從VC6到VC7的要好些。如果你使用VC8編譯示例時遇到了任何疑惑,請在討論板發帖。
手冊緒言
所謂shell擴展就是能增加某些功能到Windows資源管理器的COM對象。Shell擴展有很多內容,但關于它們的文檔資料卻非常少見。(自從我最先發表這份手冊的六年來,我相信情況要好多了。)如果你想深入Windows shell的內部,極力推薦Dino Esposito的巨作Visual C++ Windows Shell Programming (ISBN 1861001843)。對于沒有這本書的人,或者僅僅對shell 擴展感興趣的朋友,我將給你一個驚喜:一本有關shell 擴展編程的傻瓜手冊。即使本手冊并未讓你感到驚喜,那么,對你理解如何編寫shell擴展也會提供很好的幫助。本手冊假定你理解并掌握了COM和ATL的基本原理和應用。如果你還需要學習COM基本原理,請參考Intro to COM。
第一節介紹了shell擴展的概要,并提供了一個上下文菜單擴展的示例,使你對后面的章節充滿興趣。
從字面上看,shell擴展包括兩個方面:shell和擴展。所謂shell,就是資源管理器Explorer;而擴展就是指在預定的事件發生時由Explorer調用執行的代碼(比如,在.DOC文件上右擊)。因此,shell擴展就是為Explorer增添功能的COM對象。
shell擴展是一個進程內服務器,它實現了跟Explorer通信的接口。ATL是設計一個shell擴展,并使之運行的最簡單辦法;這樣你就不用為一遍又一遍的編寫QueryInterface()
和AddRef()
而大傷腦筋。在Windows NT下調試shell擴展要更容易些,這點,我在后面還會談到。
Shell擴展有很多種類型,每一類型都有其被調用的時機:即每種類型在不同的事件發生時被調用執行。下表列出了一些較常見的類型,以及它們被調用的情況:
類型 |
被調用的時機 |
它可以做什么 |
Context menu擴展處理器 |
用戶在文件對象或文件夾對象或目錄窗口背景(需要shell v 4.71+以上)單擊右鍵 |
在上下文菜單中添加菜單項 |
Property sheet擴展處理器 |
文件屬性對話框顯示時 |
在屬性對話框中定制屬性頁 |
Drag and drop擴展處理器 |
用戶用右鍵拖放文件到文件夾窗口或桌面時 |
在上下文菜單中添加菜單項 |
Drop handler擴展處理器 |
用戶拖對象并將其放到文件上時 |
任何你想做的 |
QueryInfo 擴展處理器 (需要shell version 4.71+) |
用戶在文件、“我的電腦”等其他shell對象的圖標上懸停時 |
返回一個Explorer顯示在工具提示中的字符串 |
第一節緒言
現在,你可能有很多的疑問:為什么擴展看起來像Explorer?它到底是什么樣的?一個例子就是WinZip(或者WinRAR,我沒安裝WinZip ^_^――譯者)——它包含了多種shell擴展,其中之一就是上下文擴展。下圖是WinZip(其實是WinRA的 ^_^――譯者)為壓縮文件在上下文菜單中添加的菜單項:

WinZip編寫了增加菜單項的代碼,提供了Explorer狀態欄上的菜單項幫助提示(fly-by help),并在用戶選擇一個WinZip菜單命令時執行相應的操作。
WinZip還提供了拖曳擴展處理,此類型跟上下文菜單擴展非常相似,但它是在用戶通過右鍵拖曳文件時才被觸發。下圖是WinZip(也是WinRA的 ^_^――譯者)拖曳文件彈出的菜單項:

還有很多的shell擴展類型,Microsoft不斷向每一個新的Windows版本中增加更多的類型。現在,讓我們把注意力放到上下文菜單擴展上,因為它易于編寫,效果也很明顯(能夠立即讓你滿意)。
在動手編碼之前,有一些便于編碼和調試的小技巧:當Explorer調用shell擴展(由用戶觸發)后,shell擴展暫時駐于內存中;此時,你無法重新編譯此擴展的DLL文件。為讓Explorer更迅速卸載擴展,可以在注冊表中創建下面的鍵:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL
并設置其默認值為“1”。在Win9x平臺上,這是最好的辦法。在WinNT上,可以在下面的鍵
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
創建一個DWORD 值DesktopProcess
,也設置它的值為1。(譯者:如下圖,Win9x系統太少見了)這使得“桌面”和“任務欄”運行于一個進程,其他的Explorer窗口運行在其獨立進程。這意味著,你可以調試單個Explorer窗口,當你關閉該窗口時,相關的擴展DLL就會被自動卸載,這樣就避免了DLL文件正被Windows使用而無法替換的問題。要使注冊表修改生效,需要注銷后重新登錄。

稍后,我將說明Win9x下如何進行調試。
使用AppWizard開始
我們先做一個簡單的擴展,它僅僅彈出一個消息框以表明工作正常。我們把它關聯到文本文件,這樣,當我們在一個文本文件上右擊時,該擴展就會被調用。
好了,讓我們開始吧!什么?我還沒有告訴你如何使用那些神秘的shell擴展接口?別著急,我會邊進行邊解釋。我覺得,給出一個概念,緊跟著一個示例代碼,這樣做有助于理解。當然,我也可以先解釋所有的概念,然后列出示例代碼,不過這樣很難吸引注意力。不管怎樣,開啟你的VC,我們要開始了。
運行AppWizard,生成一個名為“SimpleExt”的ATL COM工程:
去掉屬性化,保留其它默認選項,點擊“完成”。

現在,我們有了一個空的ATL項目,它可以編譯生成一個DLL,但我們還需要添加shell擴展COM對象。在類視圖中,右擊“SimpleExt”項,選擇“新建ATL 對象” (VC7,選擇“添加”→“添加類”,下圖。本文的環境是Windows XP+VC7.1,因此附圖都是VC7的)。

在ATL對象向導中,第一頁已經選擇了“簡單對象”,點擊“下一步”。

在第二頁,在“簡稱”編輯框中輸入“SimpleShlExt”(其他編輯框會自動完成):

在默認情況下,向導將創建以C和腳本客戶端為基礎的OLE自動化兼容的COM對象。我們的擴展僅僅由Explorer調用,因此我們去掉自動化支持。在“屬性”頁,選擇“接口”類型為“自定義”,并且選擇“聚合”為“否”:

點擊“完成”,就創建了CSimpleShlExt
類,它包含了實現COM對象的最基本代碼。我們將向這個類加入代碼。
初始化接口
當我們的shell擴展被加載時,Explorer將調用QueryInterface()
函數,以取得IShellExtInit
接口指針。該接口僅有一個方法Initialize()
,其函數原型如下:
HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID )
Explorer通過該方法向我們傳遞各種各樣的信息。pidlFolder
是用戶所操作文件所在的文件夾的PIDL(PIDL [pointer to an ID list],指向ID列表的指針,是一個數據結構,它唯一標識了在shell空間的任何對象,這個對象是或者不是文件系統的對象。) pDataObj
是一個IDataObj
接口變量,通過它可以取得用戶正操作的文件名。 hProgID
是一個HKEY
接口變量,通過它可以取得擴展DLL的注冊信息。在本例中,僅僅需要pDataObj
參數。
要添加這一方法到我們的COM對象,打開文件SimpleShlExt,加入下列粗體的行。AppWizard生成了一些不必需的COM關系代碼,既然我們不實現我們自己的接口,所以我指出這些失敗的能被移除的代碼(帶刪除線的那些):
#include <shlobj.h>
#include <comdef.h>
class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
public ISimpleShlExt,
public IShellExtInit
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
COM_MAP
是ATL實現QueryInterface
的宏,它告訴ATL其它程序能從COM對象取得哪些接口。
在類聲明中,加入Initialize
函數。此外,還需要一個變量來保存文件名:
protected:
TCHAR m_szFile[MAX_PATH];
public:
// IShellExtInit
STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
接著,在文件SimpleShlExt.cpp中,添加Initialize
的實現代碼:
STDMETHODIMP CSimpleShlExt::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID)
我們要做的是取得右鍵單擊選中的文件名,并把它顯示在消息框中。如果選中了多個文件,可以通過pDataObj
接口指針來訪問它們,不過為了保持例子的簡單,我們只獲取第一個文件名。
文件名的格式和拖曳文件到WS_EX_ACCEPTFILES
風格的窗口是的文件名格式一致,這樣說來,我們可以通過同樣的API:DragQueryFile
來取得文件名。我們先取得包含在IDataObject
中的數據句柄:
void CSimpleShlExt::Initialize (LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
// 在數據對象內查找CF_HDROP類型數據。
// 如果沒有數據,返回一個錯誤(“無效參數”)給Explorer。
if ( FAILED( pDataObj->GetData ( &fmt, &stg ) ))
return E_INVALIDARG;
// 取得指向實際數據的指針。
hDrop = (HDROP) GlobalLock ( stg.hGlobal );
// 確保非NULL
if ( NULL == hDrop )
return E_INVALIDARG;
注意,錯誤檢查是極其重要的,尤其是對指針的檢查。因為我們的擴展運行于Explorer進程空間,如果我們的程序掛了,Explorer會跟著掛。在Win9x系統上,這樣的崩潰可能導致需要重啟系統。
現在,我們有了HDROP
句柄,可以取得所需的文件名了:
// 有效性檢查,至少有一個文件名
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
HRESULT hr = S_OK;
if ( 0 == uNumFiles )
{
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
// 取得第一個文件名,保存到 m_szFile
if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ) )
hr = E_INVALIDARG;
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return hr;
}
如果返回E_INVALIDARG
,Explorer不會在右鍵事件時再調用我們的擴展。如果返回S_OK
,Explorer將再次調用QueryInterface()
,以取得另一接口:IContextMenu
。
與上下文菜單交互的接口
一旦Explorer初始化了我們的擴展,它將調用IContextMenu
方法來增加菜單項、提供狀態欄幫助(fly-by help),以及響應用戶的選擇。
添加IContextMenu
接口與IShellExtInit
相類似。打開文件SimpleShlExt.h,加入下列加粗的行:
class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
public IShellExtInit,
public IContextMenu
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
接著,添加IContextMenu
方法的函數原型:
public:
// IContextMenu
STDMETHODIMP GetCommandString (UINT, UINT, UINT*, LPSTR, UINT);
STDMETHODIMP InvokeCommand (LPCMINVOKECOMMANDINFO);
STDMETHODIMP QueryContextMenu (HMENU, UINT, UINT, UINT, UINT);
修改上下文菜單
IContextMenu
有三個方法。第一個QueryContextMenu()
修改上下文菜單。它的原型如下:
HRESULT IContextMenu::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags );
hmenu
是上下文菜單句柄。uMenuIndex
是我們要添加菜單項的開始位置。uidFirstCmd
和uidLastCmd
是菜單命令ID值范圍。uFlags
表明Explorer調用QueryContextMenu()
的緣由,這個后面還會談到。
關于此方法的返回值,你翻閱不同的文檔,可能得到不同的答案。Dino Esposito在他的書中認為這個返回值是所添加的菜單項的數目。但VC6的MSDN卻說它是最后一個菜單項的命令ID加1;然而,聯機MSDN卻說:
如果函數成功,返回的HRESULT值就是分配的菜單項命令ID的最大差值加1。例如,uidFirstCmd
是5,你添加了3個菜單項,它們的命令ID分別是5、7、8。那么,返回值應該是MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)。否則,返回一個OLE錯誤。
一直以來,我都按照Dino的解釋來編寫代碼,這些代碼工作地很好。實際上,他的解釋與聯機MSDN是一致的,只要將uidFirstCmd
作為第一項菜單項ID,后續的菜單項依次累加1。
我們這里的擴展簡單的加入一個菜單項,因此QueryContextMenu()
函數非常簡單:
STDMETHODIMP CSimpleShlExt::QueryContextMenu (
HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
UINT uidLastCmd, UINT uFlags )
{
// 如果標識包含了 CMF_DEFAULTONLY,那么,我們啥都不做
if ( uFlags & CMF_DEFAULTONLY )
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("簡單SHELL擴展測試") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
首先,我們檢查uFlags的值。在MSDN內,你能找到所有的標識和它們的解釋,但對于上下文菜單擴展而言,僅僅一個值是有意思的:即CMF_DEFAULTONLY。該標識告訴shell命名空間擴展保留默認的菜單項;(如果設置了它的話,)shell擴展將不增加任何的菜單項。這也是我們為什么返回0的原因。如果未設置它,我們就可以通過句柄hmenu來修改菜單,并返回1告訴shell增加了一個菜單項。
在狀態欄顯示提示幫助(fly-by help)
下一個要被調用的IContextMenu
方法是GetCommandString()
。當用戶在Explorer窗口中右擊文本文件,或者選中文本文件后點擊“文件”菜單,鼠標指到我們添加的菜單項時,狀態欄將顯示提示信息。GetCommandString()
函數返回一個字符串供Explorer顯示。
GetCommandString()
原型如下:
HRESULT IContextMenu::GetCommandString (
UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszName, UINT cchMax );
idCmd是基于0的計數器,它表明了被選中的菜單項。由于我們只有一個菜單項,所以idCmd總為0。不過,如果我們添加了,比如說,3個菜單項,idCmd就是0、1、2。uFlags是另外的一組標識,這個留待后面再討論。pwReserved可以被忽略。pszName是shell所有的緩沖區,用于顯示的幫助信息將拷貝到它中。cchMax是上述緩沖區的尺寸。返回值是HRESULT常量,比如說S_OK或E_FAIL。
GetCommandString()
也能用來取得菜單項的“動作”(verb)。動作是與語言無關的串,它標識了能作用于文件對象的動作。關于這點,ShellExecute()
的文檔中有更詳細的說明;有關動作的內容最好留待另外的文章(可以就這方面的內容另外一篇文章),這里簡要的說,是列在注冊表中的動作(比如“打開”和“打印”),或者有上下文菜單擴展動態創建的動作。這可以通過ShellExecute()
來調用shell擴展的動作。
總之,我7li8li的說了這么多,就是為了解釋清楚GetCommandString()
的作用。如果Explorer要提示信息,我們就給它;如果Explorer請求一個動作,就忽視它。這就是uFlags的作用。如果uFlags設置了GCS_HELPTEXT
位,Explorer請求提示信息。另外,如果uFlags設置了GCS_UNICODE
,我們必須給它一個UNICODE串。
本例中GetCommandString()
如下:
#include <atlconv.h> // ATL串轉換宏
STDMETHODIMP CSimpleShlExt::GetCommandString (
UINT idCmd, UINT uFlags, UINT* pwReserved, LPSTR pszName, UINT cchMax )
{
USES_CONVERSION;
// 由于這里只有一個菜單項,所以idCmd 必須為0
if ( 0 != idCmd )
return E_INVALIDARG;
// 如果Explorer請求提示信息,拷貝串到提供的緩沖區
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("簡單的SHEEL擴展幫助(fly-by help)");
if ( uFlags & GCS_UNICODE )
{
// 這里,需要把 pszName 轉換為 UNICODE
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// ANSI版本
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
這沒什么奇怪的;我使用了硬編碼并將它轉換為合適的字符集。如果你從沒用過ATL轉換宏,你最好去學一下。它們在你向COM方法和OLE函數傳遞參數時,十分有用。
一個需要注意的重要事項是,lstrcpyn()
函數保證字符串是以NULL結束的。這和CRT(C運行時)函數strncpy()
不同,后者在源串的長度大于等于cchMax時并不在串最后插入NULL。我建議總是使用lstrcpyn()
,這樣就無需在調用strncpy()
后總是檢查以確保串是否以NULL結束。
執行用戶的選擇
IContextMenu
接口最后的方法是InvokeCommand()
。此方法在用戶點擊我們增加的菜單項后被調用,其函數原型如下:
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
結構CMINVOKECOMMANDINFO有9個成員,就我們的目的而言,僅僅需要關注lpVerb
和hwnds
。lpVerb
有兩個用途:它既可以是被引發的動作名,也可以是被點擊的菜單向索引。hwnds
是用戶引發我們的擴展時所在的Explorer窗口句柄;我們可以將其作為我們用來顯示信息的窗口的父窗口。
由于我們只有一個菜單項,所以只需要檢查lpVerb
:如果它為0,那么我們的菜單項就被選中了。最簡單的事情是彈出消息框,這里的代碼也就能干這事兒。消息框顯示了被選中的文件名,這表明代碼工作正常。
STDMETHODIMP CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// 如果 lpVerb 指向一個實際串,忽略此次調用并退出
if ( 0 != HIWORD( pCmdInfo->lpVerb ) )
return E_INVALIDARG;
// 取得命令索引,這里,唯一有效的值為0
switch ( LOWORD( pCmdInfo->lpVerb) )
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("被選中的文件:\n\n%s"), m_szFile );
MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"),
MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
}
其它代碼細節
這里,集中說明如何移除AppWizard生成的多余的OLE自動化特性方面的代碼。首先,可以移除SimpleShlExt.rgs(這個文件的用途在下一節詳述)中些注冊表入口:
HKCR
{
SimpleExt.SimpleShlExt.1 = s 'SimpleShlExt Class'
{
CLSID = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
}
SimpleExt.SimpleShlExt = s 'SimpleShlExt Class'
{
CLSID = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
CurVer = s 'SimpleExt.SimpleShlExt.1'
}
NoRemove CLSID
{
ForceRemove {1CE1EBEB-1254-4880-B807-809CC31E8D2C} = s 'SimpleShlExt Class'
{
ProgID = s 'SimpleExt.SimpleShlExt.1'
VersionIndependentProgID = s 'SimpleExt.SimpleShlExt'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
val AppID = s '%APPID%'
'TypeLib' = s '{172391D4-B01E-4EF5-AC3E-34C99889D8B0}'
}
}
}
我們也能移除DLL資源中的類型庫。(VC7)在“資源視圖”中,選中“SimpleExt.rc”,右擊,選中“資源包括”:

在“資源包括”對話框的“編譯時指令”中有一行類型庫包括:

移除這行,VC彈出警告,點擊“確定”:

移除類型庫后,我們還需要修改兩處代碼,以告訴ATL,它不應通過類型庫來處理。在文件SimpleExt.cpp中的DllRegisterServer()
/DllUnregisterServer()
函數,設置RegisterServer()
/UnregisterServer()
的參數為。
STDAPI DllRegisterServer (void)
{
// ...
return _Module.RegisterServer(TRUE FALSE);
}
STDAPI DllUnregisterServer (void)
{
// ...
return _Module.UnregisterServer(TRUE FALSE);
}
注冊Shell擴展
現在,我們實現所有的COM接口。不過,怎么才能讓Explorer使用我們的擴展呢?ATL自動生成注冊COMD服務器DLL 的代碼,但那是給其它程序使用。為了讓Explorer知道擴展存在,需要在文本文件的下述注冊表鍵下注冊我們的擴展:
HKEY_CLASSES_ROOT\txtfile
在這個注冊表鍵下,名為ShellEx
的鍵保存了有關文本文件的shell擴展列表;在它的下一級,名為ContextMenuHandlers
的鍵保存了上下文菜單擴展列表。每個擴展都擁有一個ContextMenuHandlers
的子鍵,其默認值為shell擴展的GUID。本文簡單擴展的示例,將創建如下子鍵:
HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt
并設置其默認值為擴展COM的GUID,如“{1CE1EBEB-1254-4880-B807-809CC31E8D2C }”。
不必編寫代碼來完成COM的注冊。在“解決方案管理器”中有文件SimpleShlExt.rgs;這是一個文本文件,它被ATL解析,指導ATL在該服務器注冊時添加哪些鍵,在卸載時刪除哪些鍵。下面是注冊該擴展所要添加的注冊表鍵:
HKCR
{
NoRemove txtfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
}
}
}
}
每一行都代表一個注冊鍵名。“HKCR”是HKEY_CLASSES_ROOT
的縮寫。關鍵字NoRemove
表明這個鍵在服務器卸載時不用刪除。關鍵字ForceRemove
表明在寫入新鍵之前,如果該鍵存在,那么就要先刪除它;這行剩下的部分指定了一個字符串(這就是“s”的意思),它是SimpleShlExt
的默認值。
這里,我插入幾句。我們的擴展注冊在HKEY_CLASSES_ROOT\txtfile
下;然而,“txtfile
”并不是持久的或者預先定好的名字。如果你查看一下HKEY_CLASSES_ROOT\.txt
,它的默認值就是“txtfile
”。這樣,有兩個副作用:
1、 既然“txtfile
”可能不是正確的鍵名,因此RGS腳本并不可靠。
2、 某些文件編輯軟件可能在安裝到系統時,就把它自身關聯到文本文件。如果它改變了HKEY_CLASSES_ROOT\.txt
的默認值,那么所有的shell擴展就不能用了。
在我看來,這確實是一個設計上的漏洞。Microsoft可能也這么看,因為最新的擴展,如QueryInfo擴展就注冊在HKEY_CLASSES_ROOT\.txt
下。
好了,到此為止。還有一個注冊的細節,即在WinNT上,為了讓非管理員帳號也能使用我們的擴展,得把它放到“approved ”擴展列表中:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved
在這個鍵下,創建以擴展的GUID為名的串值,其內容任意。代碼在DllRegisterServer()
和DllUnregisterServer()
中,都是一些簡單的注冊表訪問,我就不羅列了。你可以在示例代碼中找到。
調試Shell擴展
總有一天,你將寫一個不是這么簡單的shell擴展;那時,你不得不調試它。打開項目的屬性對話框,在“調試”的“命令”編輯框輸入“C:\windows\explorer.exe”。如果是WinNT系統,設置DesktopProcess
鍵(前述),當你按F5時就啟動了一個新的Explorer窗口。只要是在這個窗口完成所有的工作,那么在關閉這個窗口時,擴展就會被卸載,這樣就不影響后面的重建DLL。
在Win9x上,在運行調試器前,必須關閉shell:點擊“開始”,點擊“注銷”。按下Ctlr+Alt+Shift,并點“取消”。這將關閉Explorer,任務欄(桌面)消失了。切換到MSVC,按F5開始調試。要中止調試,按下“Shift+F5”關閉Explorer。完成調試后,可以從“開始”“運行”“Explorer.exe”,讓其正常啟動。
擴展的外觀
增加擴展后的上下文菜單項

Explorer狀態欄提示(fly-by help)

彈出的消息框,顯示了所選中的文件名

版權與許可
作者:Michael Dunn
譯者:Yesaidu