當用戶右擊一個shell對象時,shell會顯示它的上下文菜單。文件系統對象有大量的標准菜單項,如"剪切"和"拷貝",這些都是缺省的菜單項。如果對象是一個文件,是文件類的成員,就能夠在注冊表裡指定附加的菜單項。Shell檢查注冊表,看看文件類型是否與一些上下文菜單handler相關聯,如果是,shell會咨詢這些handler是否添加額外的菜單項。
上下文菜單handler是一種shell擴展handler,它添加命令到已有的上下文菜單中。上下文菜單handler都與特定的文件類相關聯,並且在顯示這類文件的成員的上下文菜單時調用。通過實現和注冊這樣一個handler,能夠動態地添加菜單項到對象的上下文菜單上,從而為特殊的對象定制菜單。
上下文菜單Handler的工作原理
作為一種shell擴展handler,上下文菜單handler同所有其它handler一樣, 是進程內COM 對象,即對象作為動態連接庫 (DLL)實現。除了IUnknown接口外,上下文菜單還必須導出IShellExtInit和IContextMenu接口,作為選擇,上下文菜單也能導出IContextMenu2和IContextMenu3,這些接口可以實現自畫菜單項。
IShellExtInit接口僅僅被shell用來初始化handler,主要的操作通過handler的IContextMenu接口進行。Shell首先調用IContextMenu::QueryContextMenu,傳送一個HMENU句柄,這個方法用它來增加上下文菜單。如果用戶亮選了這些新添加的某個命令項, IContextMenu::GetCommandString將被調用,以取得這條菜單的幫助信息,把它顯示在資源管理器的狀態條上。如果用戶單擊了handler的條目,shell調用IContextMenu::InvokeCommand,從而handler能夠執行合適的操作。
實現IContextMenu接口
1、實現QueryContextMenu方法
Shell通過調用IContextMenu::QueryContextMenu,允許handler把它的菜單項添加到菜單中。QueryContextMenu共有5個參數,各參數作用如下:
1) Hmenu:HMENU類型,表示上下文菜單的句柄。
2) IndexMenu:第一個被添加的菜單索引。
3) IdCmdFirst:添加的菜單ID初值。
4) idCmdLast:添加的菜單ID最大值。
5) uFlags:與上下文菜單相關的狀態標志,共有3種,如下:
CMF_DEFAULTONLY 用戶選擇了缺省的命令,通常是通過雙擊對象產生。QueryContextMenu 在把控制返回給shell前不應該修改菜單。
CMF_NODEFAULT 菜單沒有缺省的條目,這個方法應該把它的命令加到菜單中。
CMF_NORMAL 上下文菜單將被正常顯示,這個方法應該把它的命令加到菜單中。
必須注意的是,任何添加的菜單項的ID必須落在idCmdFirst和idCmdLast兩個參數中間,通常,添加的第一個菜單項ID設為idCmdFirst,以後每添加一個菜單項,就把ID加1,這樣,即使shell調用了不止一個handler,也可以確保菜單項的ID不超過idCmdLast和可能的ID最大值。
在ID和idCmdFirst之間,菜單項ID的command offset(命令偏移)是不同的,應該保存handler添加到上下文菜單中的每個菜單項的offset,因為如果shell按順序調用GetCommandString或者InvokeCommand,可以使用它來鑒別菜單項的ID。
還應該為每一個添加的命令賦予一個verb。Verb是語言獨立的字符串,當調用InvokeCommand時,常常用verb來代替偏移以鑒別命令。
QueryContextMenu 方法使用InsertMenu或InsertMenuItem 添加新的菜單項,然後返回一個嚴格設置為SEVERITY_SUCCESS的HRESULT值,把它的值設置為被分配的最大的命令ID。例如,假如idCmdFirst是5,添加了3個菜單項,ID分別是5,7,8,則返回值應該是MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)。
以下是一個QueryContextMenu實例:
HRESULT __stdcall TAddContextMenuImpl::QueryContextMenu(HMENU hmenu,
UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
if(!(CMF_DEFAULTONLY & uFlags))
{
InsertMenu(hmenu, indexMenu, MF_STRING | MF_BYPOSITION,idCmdFirst,
_T("選擇打開方式..."));
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0));
}
2、實現GetCommandString 方法
如果用戶高亮了一個handler添加的菜單項,shell將調用handler的GetCommandString方法。這個方法需要傳遞菜單項的偏移值(ID)、指定信息類型的標志、一個預留的參數、一個字符串緩沖區以及緩沖區的大小。
一般,這個方法可以不用處理,以下示例程序直接返回S_OK。
HRESULT __stdcall TAddContextMenuImpl::GetCommandString(UINT idCmd, UINT uFlags,
UINT *pwReserved, LPSTR pszName, UINT cchMax)
{
return S_OK;
}
3、實現InvokeCommand方法
當在上下文菜單中選擇一個菜單項時,shell就會調用InvokeCommand,告訴handler運行相關聯的命令。在Shlobj.h中,參數pici被聲明為CMINVOKECOMMANDINFO結構,但實際上,它經常指向CMINVOKECOMMANDINFOEX結構,這個結構是CMINVOKECOMMANDINFO的擴展版本,有幾個成員允許傳遞Unicode字符串。
CMINVOKECOMMANDINFO的成員簡介如下:
1) cbSize :結構的大小。
2) fMask :為0,或下列標志的組合。
CMIC_MASK_ASYNCOK 在返回之前等待DDE會話結束
CMIC_MASK_FLAG_NO_UI 當執行命令時,系統防止顯示用戶接口元素(如錯誤信息)
CMIC_MASK_HOTKEY dwHotKey 成員有效
CMIC_MASK_ICON hIcon成員有效
CMIC_MASK_NO_CONSOLE 如果上下文菜單handler必須創建新進程,正常情況下將創建一個控制台,設置CMIC_MASK_NO_CONSOLE標志可以禁止創建新的控制台
3) hwnd :擁有上下文菜單窗口的句柄,handler可以使用這個句柄顯示自己的信息提示框和對話框。
4) lpVerb :32位值,高位字包含0,低位字是命令的菜單ID偏移。當用戶選擇一個菜單命令時,Shell用MAKEINTRESOURCE宏產生這個值,如果高位字不是0,那麼這個成員指向一個以NULL結尾的字符串,指出命令的語言無關的名稱,即上文的verb。典型情況下,當命令被一個應用程序激活時,這個成員是一個字符串。系統提供了下面幾個預定義的常數值:
值:CMDSTR_NEWFOLDER 字符串:"NewFolder"
值:CMDSTR_VIEWDETAILS 字符串:"ViewDetails"
值:CMDSTR_VIEWLIST 字符串:"ViewList"
5) lpParameters :命令傳送的參數字符串,對於shell擴展插入的菜單項,這個成員總是NULL。
6) lpDirectory :目錄名稱,對於shell擴展插入的菜單項,這個成員總是NULL。
7) nShow :顯示窗口或啟動應用程序時,傳遞給ShowWindow函數的參數。
8) dwHotKey :分配給被命令激活的應用程序的熱鍵。如果fMask 不是CMIC_MASK_HOTKEY,這個成員被忽略。
9) hIcon :被命令激活的應用程序使用的圖標。如果fMask 不是CMIC_MASK_ICON,這個成員被忽略。
以下示例先打開一個"選擇文件"的對話框,然後用所選擇的程序打開在資源管理器中被選擇的文件。為了簡化,假定在資源管理器只選擇了一個文件。
HRESULT __stdcall TAddContextMenuImpl::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
{
if(HIWORD(pici->lpVerb)==0)
{
if(LOWORD(pici->lpVerb)==0) // 添加的第一個菜單項
{
TOpenDialog *Dlg=new TOpenDialog(NULL);
Dlg->Title="打開\"";
Dlg->Title=Dlg->Title+g_szFilePath+"\"";
Dlg->Options.Clear();
Dlg->Options << ofFileMustExist << ofPathMustExist << ofNoChangeDir;
if(Dlg->Execute())
{
ShellExecute(pici->hwnd,"open",Dlg->FileName.c_str(),g_szFilePath,NULL,SW_SHOW); }
return S_OK;
}
}
return S_FALSE;
}
注冊上下文菜單Handler
上下文菜單與文件類或者文件夾相關聯。對於文件類,handler注冊在文件類的HKEY_CLASSES_ROOT\ProgID\Shellex\ContextMenuHandlers子鍵下。在ContextMenuHandlers下創建一個以handler子鍵,把子鍵的缺省值設置為handler的CLSID的字符串值,就可以完成注冊。
也能夠把handler關聯到文件夾,注冊的方法與上面類似,不過是在HKEY_CLASSES_ROOT\FolderType\Shellex\ContextMenuHandlers增加子鍵, 其中的FolderType 是文件夾類型的名稱。
如果一個文件類有上下文菜單與它關聯,那麼雙擊一個對象將自動啟動缺省的命令,而不會調用handler的QueryContextMenu方法。當對象被雙擊時,為了指定調用handler的QueryContextMenu方法,必須在handler的CLSID鍵下創建一個ShellEx\MayChangeDefaultMenu的子鍵。這樣,當與handler關聯的對象被雙擊時,QueryContextMenu 被調用,而且uFlags參數會包含CMF_DEFAULTONLY 標志。
注意,如果設置了MayChangeDefaultMenu鍵,當一個關聯的項目被雙擊時,會強制系統載入handler的DLL。如果handler不改變缺省動作,就不應該設置MayChangeDefaultMenu,否則會引起系統不必要地載入這個DLL。僅僅當在可能改變上下文菜單的缺省動作時,才應該在設置上下文菜單handler的這個值。
創建工程
作為Borland的產品,用C++ Builder創建shell擴展的過程與Delphi有類似之處,但它畢竟是C++語言,所以也有與VC類似之的地方。
1. 選擇File菜單的New菜單項,翻到New Items對話框的ActiveX頁,雙擊ActiveX Library項,創建一個新的COM工程,把工程命名為MyContextMenu。從New Items 對話框的ActiveX頁選擇COM Object項,將打開COM Server向導。把"COClass"改為AddContextMenu,選擇Apartment線程模式。其它不要改寫。C++ Builder自動產生一個接口和一個類。默認的類名是TAddContextMenuImpl,采用自動生成的IAddContextMenu接口。我們必須自己添加新的接口IShellExtInit和IContextMenu,如下所示,粗體是添加的內容:
#include <shlobj.h> // 聲明IShellExtInit和IContextMenu的頭文件
class ATL_NO_VTABLE TAddContextMenuImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<TAddContextMenuImpl, &CLSID_AddContextMenu>,
public IShellExtInit,
public IContextMenu,
public IAddContextMenu
{
private:
char g_szFilePath[MAX_PATH];
public:
… …
BEGIN_COM_MAP(TAddContextMenuImpl)
COM_INTERFACE_ENTRY(IAddContextMenu)
COM_INTERFACE_ENTRY(IContextMenu) // 導出IContextMenu接口
COM_INTERFACE_ENTRY(IShellExtInit) // 導出IShellExtInit接口
END_COM_MAP()
… …
};
2. 實現IShellExtInit接口的Initialize方法,在類定義中增加如下內容:
STDMETHOD (Initialize)(LPCITEMIDLIST pidlFolder,LPDATAOBJECT lpdobj,HKEY hkeyProgID);
Initialize方法的代碼如下,從lpdobj對象中取出資源管理器中選擇的文件名,程序假定只選擇了一個文件。
HRESULT __stdcall TAddContextMenuImpl::
Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj, HKEY hkeyProgID)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
if (FAILED(lpdobj->GetData(&fmt, &stg))) return E_FAIL;
hDrop = (HDROP)GlobalLock(stg.hGlobal);
if ( hDrop == NULL)
{
ReleaseStgMedium(&stg);
return E_OUTOFMEMORY;
}
DragQueryFile(hDrop, 0, g_szFilePath, MAX_PATH);
GlobalUnlock(stg.hGlobal);
ReleaseStgMedium(&stg);
return S_OK;
}
3. 實現IContextMenu接口的各個方法,內容如上文所示,聲明如下:
public:
STDMETHOD (QueryContextMenu)(HMENU hmenu,UINT indexMenu,UINT idCmdFirst,UINT idCmdLast,UINT uFlags);
STDMETHOD (InvokeCommand)(LPCMINVOKECOMMANDINFO pici);
STDMETHOD (GetCommandString)(UINT idCmd,UINT uFlags,UINT *pwReserved,LPSTR pszName,UINT cchMax);
最後,把工程編譯為DLL文件,運行菜單[Run->Register ActiveX Server],把DLL注冊。與Delphi和VC相比,C++ Builder似乎有些缺陷。首先,它實現時太過復雜,生成的文件一大堆。最麻煩的是,它無法實現自動注冊為shell擴展,它沒有VC的rgs文件,像Delphi那樣改寫UpdateRegistry函數,怎麼也不行,好像這個函數沒有調用一樣。無奈,只好自己動手向注冊表添加必須的項目(如圖)。但是,C++ Builder給出了3個CLSID,很迷惑人,正確的CLSID應該是類AddContextMenu的,C++ Builder給它命名為CLSID_AddContextMenu。
注冊後,在資源管理器右擊任何文件,如readme.txt,都將打開一個選擇文件的對話框,然後shell用選擇的文件打開readme.txt。