Shell擴充套件程式設計實現Windows2000桌面圖示透明

sinall發表於2005-05-24

(本文根據《Windows Shell擴充套件程式設計完全指南》改寫)

開始編寫上下文選單 它該做些什麼?
開頭先讓我們做簡單一些, 只彈出一個對話方塊以表明當前的擴充套件能夠正常地工作.
我們把擴充套件關聯到 .TXT 檔案, 因此當使用者右鍵單擊文字檔案物件時擴充套件就會被呼叫
.

使用 AppWizard 開始
好吧, 讓我們開始吧! 什麼? 我還沒告訴你怎樣使用那些神祕的 shell 擴充套件介面?
彆著急, 我會邊進行邊解釋的。

我覺得先解釋一下一個概念再緊接著說明示例程式碼,對理解例子程式會更簡單一些. 當然我也可以把所有的東西都先解釋完,然後再解釋程式碼, 但我覺得這樣做不能吸引人的注意力。不管怎麼樣, VC開火,開始!

執行AppWizard,生成一個名為SimpleExt ATL COM 工程. 保留所有預設的設定選項,點選”完成”
.
現在我們已經有了一個空的 ATL工程,它可以編譯並生成一個 DLL, 但我們還需要新增Shell擴充套件的 COM 物件
.
ClassView , 右擊 SimpleExt classes 條目, 選擇 New ATL Object.

ATL Object Wizard, 第一頁預設已經選擇了 Simple Object , 所以單擊 Next 即可.
在第二頁中, Short Name 文字框裡輸入 SimpleShlExt ,點選 OK. (其餘的文字框會自動填充完
.)
這樣就建立了一個名為 CSimpleShlExt 的類,其包含了實現COM物件最基本的程式碼. 我們將在這個類中加入我們自己的程式碼
.

初始化介面
當我們的shell擴充套件被載入時, Explorer 將呼叫我們所實現的COM物件的 QueryInterface() 函式以取得一個 IShellExtInit 介面指標.
該介面僅有一個方法 Initialize(), 其函式原型為:

HRESULT IShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID );


Explorer
使用該方法傳遞給我們各種各樣的資訊.
PidlFolder
是使用者所選擇操作的檔案所在的資料夾的 PIDL 變數. (一個 PIDL [指向ID 列表的指標] 是一個資料結構,它唯一地標識了在Shell名稱空間的任何物件, 一個Shell名稱空間中的物件可以是也可以不是真實的檔案系統中的物件
.)
pDataObj
是一個 IDataObject 介面指標,通過它我們可以獲取使用者所選擇操作的檔名。

hProgID
是一個HKEY 登錄檔鍵變數,可以用它獲取我們的DLL的註冊資料.
在這個簡單的擴充套件例子中, 我們將只使用到 pDataObj 引數.

要新增這個介面進 COM 物件, 先開啟SimpleShlExt.h 檔案, 然後加入下列標紅的程式碼:

#include "shlobj.h"
#include "comdef.h"

class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit

BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()

COM_MAP是ATL實現 QueryInterface()機制的巨集,它包含的列表告訴ATL其它外部程式用QueryInterface()能從我們的 COM物件獲取哪些介面.
接著,在類宣告裡, 加入Initialize()的函式原型.
另外我們需要一個變數來儲存檔名:

protected:
TCHAR m_szFile [MAX_PATH];
public:
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

然後, 在 SimpleShlExt.cpp 檔案中, 加入該函式方法的實現定義:

HRESULT CSimpleShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID )

我們要做的是取得當前滑鼠所在的視窗,並把它和桌面上的ListView

做比較,如果二者不同,則滑鼠是在其他Dictionary上點選,不新增

選單,直接返回:

{
         HWND Wnd;

         Wnd=::GetDesktopWindow();

         Wnd=FindWindowEx(Wnd, 0, "Progman", NULL);

         Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL);

         Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL);

 

         POINT Point;

         ::GetCursorPos(&Point);

 

         if(::WindowFromPoint(Point)!=Wnd)

                   return E_INVALIDARG;

 

         return S_OK;

}


要是我們返回 E_INVALIDARG, Explorer 將不會繼續呼叫以後的擴充套件程式碼.
要是返回 S_OK, Explorer 將再一次呼叫QueryInterface() 獲取另一個我們下面就要新增的介面指標
: IContextMenu.

與上下文選單互動的介面

一旦 Explorer 初始化了擴充套件,它就會接著呼叫 IContextMenu 的方法讓我們新增選單項, 提供狀態列上的提示, 並響應執行使用者的選擇.

新增IContextMenu 介面到Shell擴充套件類似於上面IshellExtInit介面的新增 .開啟 SimpleShlExt.h,新增下列標紅的程式碼:

class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit,
public IContextMenu

{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)

END_COM_MAP()


新增 IContextMenu 方法的函式原型:

public:
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(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.0所帶的MSDN 又說它是我們新增的最後一個選單項的命令ID加上
1.
而最新的 MSDN 又說
:
將返回值設為你為各選單項分配的命令ID的最大差值,加上
1.
例如, 假設 idCmdFirst 設為5,而你新增了三個選單項 ,命令ID分別為 5, 7,
8.
這時返回值就應該是:
MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1).

我是一直按 Dino 的解釋來做的, 而且工作得很好
.
實際上, 他的方法與最新的 MSDN 是一致的, 只要你嚴格地使用 uidFirstCmd作為第一個選單項的ID,再對接續的選單項ID每次加
1.

我們暫時的擴充套件僅加入一個選單項,所以 QueryContextMenu() 非常簡單
:


首先我們檢查 uFlags.
你可以在 MSDN中找到所有標誌的解釋, 但對於上下文選單擴充套件而言, 只有一個值是重要的
: CMF_DEFAULTONLY.
該標誌告訴Shell名稱空間擴充套件保留預設的選單項,這時我們的Shell擴充套件就不應該加入任何定製的選單項,這也是為什麼此時我們要返回 0 的原因
.
如果該標誌沒有被設定, 我們就可以修改選單了 (使用 hmenu 控制程式碼), 並返回 1 告訴Shell我們新增了一個選單項.

在狀態列上顯示提示幫助

下一個要被呼叫的IContextMenu 方法是 GetCommandString(). 如果使用者是在瀏覽器視窗中右擊文字檔案,或選中一個文字檔案後單擊檔案選單時,狀態列會顯示提示幫助.
我們的 GetCommandString() 函式將返回一個幫助字串供瀏覽器顯示
.

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
是該緩衝區的大小
.
返回值是S_OK E_FAIL.

GetCommandString() 也可以被呼叫以獲取選單項的動作( "verb") .
verb
是個語言無關性字串,它標識一個可以加於檔案物件的操作。

ShellExecute()
的文件中有詳細的解釋, 而有關verb的內容足以再寫一篇文章, 簡單的解釋是:verb 可以直接列在登錄檔中( "open" "print"等字串), 也可以由上下文選單擴充套件建立. 這樣就可以通過呼叫ShellExecute()執行實現在Shell擴充套件中的程式碼.

不管怎樣, 我說了這多隻是為了解釋清楚GetCommandString() 的作用
.
如果 Explorer 要求一個幫助字串,我們就提供給它. 如果 Explorer 要求一個verb, 我們就忽略它. 這就是 uFlags 引數的作用
.
如果 uFlags 設定了GCS_HELPTEXT , Explorer 是在要求幫助字串. 而且如果 GCS_UNICODE 被設定, 我們就必須返回一個Unicode字串
.

我們的 GetCommandString() 如下
:

              USES_CONVERSION;

              //檢查 idCmd, 它必須是0,因為我們僅有一個新增的選單項.

              if ( 0 != idCmd )

                     return E_INVALIDARG;

             

              // 如果 Explorer 要求幫助字串,就將它拷貝到提供的緩衝區中.

              if ( uFlags & GCS_HELPTEXT )

              {

                     LPCTSTR szText = _T("透明圖示");             

                     if ( uFlags & GCS_UNICODE )

                     {

                            // 我們需要將 pszName 轉化為一個 Unicode 字串, 接著使用Unicode字串拷貝 API.

                            lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );

                     }

                     else

                     {

                            // 使用 ANSI 字串拷貝API 來返回幫助字串.

                            lstrcpynA ( pszName, T2CA(szText), cchMax );

                     }

                     return S_OK;

              }

              return E_INVALIDARG;

這裡沒有什麼特別的程式碼; 我用了硬編碼的字串並把它轉換為相應的字符集.
如果你從未使用過ATL字串轉化巨集,你一定要學一下,因為當你傳遞Unicode字串到COMOLE函式時,使用轉化巨集會很有幫助的
.
我在上面的程式碼中使用了T2CW T2CA TCHAR 字串分別轉化為Unicode ANSI字串
.
函式開頭處的USES_CONVERSION 巨集其實宣告瞭一個將被轉化巨集使用的區域性變數
.

要注意的一個問題是: lstrcpyn() 保證了目標字串將以null為結束符
.
這與C執行時(CRT) strncpy()不同. 當要拷貝的源字串的長度大於或等於cchMax strncpy()不會新增一個 null 結束符
.
我建議總使用lstrcpyn(), 這樣你就不必在每一個strncpy()後加入檢查保證字元 串以 null為結束符的程式碼
.

執行使用者的選擇

IContextMenu
介面的最後一個方法是 InvokeCommand(). 當使用者點選我們新增的選單項時該方法將被呼叫. 其函式原型是:

HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );

CMINVOKECOMMANDINFO 結構帶有大量的資訊, 但我們只關心 lpVerb hwnd 這兩個成員.
lpVerb
引數有兩個作用 它或是可被激發的verb(動作), 或是被點選的選單項的索引值
.
hwnd
是使用者啟用我們的選單擴充套件時所在的瀏覽器視窗的控制程式碼
.

因為我們只有一個擴充套件的選單項, 我們只要檢查lpVerb 引數, 如果其值為0, 我們可以認定我們的選單項被點選了
.
我能想到的最簡單的程式碼就是彈出一個資訊框, 這裡的程式碼也就做了這麼多. 資訊框顯示所選的檔案的檔名以證實程式碼正確地工作
.

    // 如果lpVerb 實際指向一個字串, 忽略此次呼叫並退出.

    if ( 0 != HIWORD( pCmdInfo->lpVerb ))

       {

        return E_INVALIDARG;

       }

    // 點選的命令索引 在這裡,唯一合法的索引為0.

    switch ( LOWORD( pCmdInfo->lpVerb ))

       {

       case 0:

              {

                     HWND Wnd;

                     Wnd=::GetDesktopWindow();

                     Wnd=FindWindowEx(Wnd, 0, "Progman", NULL);

                     Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL);

                     Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL);

                     ::SendMessage(Wnd, LVM_SETTEXTBKCOLOR, 0, 0xffffffff);

                     ::InvalidateRect(Wnd, NULL, TRUE);

 

            return S_OK;

              }

        break;

       default:

              return E_INVALIDARG;

        break;

註冊Shell擴充套件
現在我們已經實現了所有需要的COM介面. 可是我們怎樣才能讓瀏覽器使用我們的擴充套件呢?
ATL
自動生成註冊COM DLL伺服器的程式碼, 但這只是讓其它程式可以使用我們的DLL.

最後,在shell版本 4.71+中, 你可以讓上下文選單在使用者右擊瀏覽器視窗(包括桌面)的背景時激發.
要讓你的擴充套件在這種情況下被激發,需要在HKCR/Directory/Background/shellex/ContextMenuHandlers 鍵下進行註冊.
使用該方法, 你可以新增定製選單到桌面或任意目錄上下文選單.
這時傳送到 IShellExtInit::Initialize()的引數有些不同,所以我將在以後的文章中講述這方面的內容.

相關文章