一、前言
在 Windows 的資源管理器窗口中,我們見過 WinZIP,WinRAR 等軟件能在文件或文件夾的默認快捷菜單中添加幾個菜單項,它可以使用戶無須進入軟件內部而直接在視窗中進行壓縮/解壓操作,十分方便用戶操作,這無疑是一個較好的應用模型,它就是我們所說的Shell擴展技術。本文將以一個普通的源代碼統計程序為例來說明怎樣實現Shell擴展技術。下面是程序的運行效果圖:
圖一 示例代碼運行效果圖一
圖二 示例代碼運行效果圖二
二、實現原理
為了在Windows的任何視窗中擴展文件或文件夾的默認菜單,我們必須使Windows在顯示快捷菜單加載我們的程序段,一般我們利用COM組件來達到這個目的。COM組件分為三種:進程內服務程序,本地服務程序,以及遠程服務程序。要想讓explorer加載並執行我們的代碼,當然得使用進程內服務程序,它的表現形式是DLL, DLL在加載後被映射到可執行程序的虛擬地址空間,我們向explorer提供一些接口,explorer將在顯示快捷菜單時調用它們時,我們可以在那些接口中做一些我們想做的事,如添加快捷菜單,實現菜單項功能等等,從而實現Shell擴展了。?
至於源代碼統計,則不難實現。這裡我以C/C++風格的源代碼為例,並應用一種最簡單的統計規則,當統計文件時,我們將代碼內容讀入緩存,判斷每一個字符是否為換行符(\n),若是,計數加1。當然我們是對文件夾進行統計更有意義,所以我們可以使用遞歸的方法遍歷文件夾內所有文件,找出有效文件(這裡我僅統計C/C++程序,所以只處理後綴名為.C、.CPP、.H 的文件),根據前面的方法一一統計即可求出文件夾內所有代碼的總行數。
三、實現過程
1.新建一個VC工程,選定ATL COM AppWizard類型,工程起名為SrcCount,進入下一步;
2.選擇服務類型為DLL(默認選項)即可,這裡不需要MFC支持(若加入MFC支持的話,編寫代碼時會方便些,但程序失去ATL短小精悍的特點了,熊掌與魚不可兼得:)),進入下一步;
3.現在會顯示工程的配置信息,我們按確定按鈕後就建立一個ATL COM組件了。
4.我們現在加入一個組件對象,在工程的快捷菜單上選擇New ATL Object…,在隨後的對話框中的種類中選擇Simple Object,單擊下一步,在“Short Name”中填寫CountLines,Attributes屬性頁中按默認選項,單擊確定按鈕。我們可以在VC的工作區裡看到已添加一個接口ICountLines。
5.為該接口添加方法,在接口的快捷菜單上按右鍵,選擇Add method…,方法名為GetFileLines,它的參數分別為:[in]BSTR *pFilePath, [out]int *lines。它的作用是統計源代碼文件的行數。下面是代碼的主要實現部分: //////////////////////////////////////////////////////////////////////////////////////////////////////////
// 作用:獲取源文件的代碼行數
// 參數:1. pFilePath :輸入參數,指定源文件的路徑;
// 2. lines:輸出參數,獲得源文件的代碼行數。
STDMETHODIMP CCountLines::GetFileLines(BSTR *pFilePath, int *lines)
{
// 存放代碼的總行數
int totalLine = 0; ?
// 打開文件
HANDLE hFile = CreateFile((TCHAR *)pFilePath, ?
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if ((HANDLE)-1 == hFile)
{
*lines = -1;
return S_FALSE;
}
// 開辟緩沖區存放文件內容
DWORD dwFileSize = GetFileSize(hFile, NULL);
BYTE *lpBuffer = new BYTE[dwFileSize];
memset(lpBuffer, 0, dwFileSize); ?
DWORD dwRead = 0;
BOOL bReadFile = ReadFile(hFile, lpBuffer, dwFileSize, &dwRead, NULL);
assert(bReadFile && dwRead == dwFileSize); ?
// 我們這裡僅用一個簡單的統計規則,即以換行符(‘\n’)為一行代碼結束的標記
for (unsigned i = 0; i < dwFileSize; i++)
{
if (lpBuffer[i] == ''\n'')
{
totalLine++;
}
}
// 釋放緩沖區
delete []lpBuffer;
CloseHandle(hFile);
// 保存代碼行數
*lines = totalLine + 1; ?
return S_OK;
}
6.繼續添加方法GetFolderLines,它將根據遞歸算法對文件夾裡的每個文件進行代碼統計,這裡就不具體寫出了,請參看源代碼。
7.在CCountLines的基類中添加IShellExtInit、IContextMenu。
8.當浏覽器explorer.exe加載我們的程序段時,將調用IShellExtInit 接口初始化菜單,然後調用接口IContexMenu處理右鍵菜單,所以我們將在DLL組件中暴露以上接口。這只需要在BEGIN_COM_MAP()與END_COM_MAP()宏中加入接口即可。
9.Windows窗口初始化快捷菜單時調用IShellExtInit接口的Initialize ()方法,函數原型如下:
HRESULT Initialize(LPCITEMIDLIST pidlFolder,LPDATAOBJECT lpdobj, HKEY hkeyProgID );
我們將在這個函數裡進行必要的初始化動作,例如保存文件名的完整路徑,保存注冊表的鍵值等。
10.浏覽器調用IContexMenu接口進行命令的解釋執行,這是我們進行源代碼統計的主要部分,我們將調用以上的算法對所選定的文件夾按照約定的規則進行代碼統計。這個接口主要有以下三個方法需要實現: // 在視窗的狀態欄上顯示命令說明,這裡值得注意的是,我們需要對ASCII碼和UNICODE碼進行處理,
// 以適應不同系統。
HRESULT GetCommandString(
UINT idCmd,
UINT uFlags,
UINT *pwReserved,
LPSTR pszName,
UINT cchMax
);
// 執行菜單明命令,在此可以實現具體的功能。
HRESULT InvokeCommand(
LPCMINVOKECOMMANDINFO pici
);
// 在這裡增加快捷菜單
HRESULT QueryContextMenu(
HMENU hmenu,
UINT indexMenu,
UINT idCmdFirst,
UINT idCmdLast,
UINT uFlags
);
這裡僅舉例 InvokeCommand()的實現,其他請看源代碼。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 作用:執行快捷菜單命令
// 參數:1. pici:包含命令信息的結構體
HRESULT CCountLines::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
{
BOOL bEx = FALSE;
BOOL bUnicode = FALSE;?
if (pici->cbSize = sizeof(CMINVOKECOMMANDINFOEX))
{
bEx = TRUE;
if ((pici->fMask & CMIC_MASK_UNICODE))
{
bUnicode = TRUE;
}
}
// lpVerb參數有兩種標識:如高位字非0,則為命令字串,否則低位提供了快捷菜單的偏移值。
if (!bUnicode && HIWORD(pici->lpVerb))
{
if(StrCmpIA(pici->lpVerb, "Stat."))
{
return E_FAIL;
}
}
else if (bUnicode && HIWORD(((CMINVOKECOMMANDINFOEX *) pici)->lpVerbW))
{
if(StrCmpIW(((CMINVOKECOMMANDINFOEX *)pici)->lpVerbW, L"Stat."))
{
return E_FAIL;
}
}
else if (LOWORD(pici->lpVerb) != IDM_SRC_COUNT)
{
return E_FAIL;
}
else
{
assert(0 == HIWORD(pici->lpVerb));
int lines = 0;
TCHAR szTitle[MAX_PATH] = {0};
TCHAR szMsg[MAX_PATH] = {0};
TCHAR szFormat[MAX_PATH] = {0};
memset(szMsg, 0, MAX_PATH);
//保存當前光標並重設為等待形狀
HCURSOR hOldCursor = GetCursor();??
HCURSOR hNewCursor = LoadCursor(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDC_COUNT_WAIT));
assert(hNewCursor);
SetCursor(hNewCursor);?
TCHAR szTemp[MAX_PATH] = {0};
LoadString(_Module.GetModuleInstance(), IDS_TOTAL_LINES, szFormat, MAX_PATH);
if (SUCCEEDED(GetFolderLines((BSTR *)&m_pszPath, &lines)))
{
wsprintf(szMsg, szFormat, (LPTSTR)m_pszPath, lines);
}
// 恢復默認光標形狀
SetCursor(hOldCursor);
// 顯示統計代碼信息
LoadString(_Module.GetModuleInstance(), IDS_TITLE, szTitle, MAX_PATH);
MessageBox(pici->hwnd, szMsg, szTitle, MB_OK | MB_ICONINFORMATION);
}
return S_OK;
}
四、其它
本程序是進程內服務程序,運行regsvr32進行注冊(注:在VC編譯器中,COM組件在編譯時會自動調用regsvr32 進行注冊,請看工程配置文件),例如,該組件已COPY至C:\WinNT\System32下,我們將輸入如下命令行注冊:
圖三 示例代碼運行效果圖三
因為是對文件夾統計,所以還需在
HKEY_CLASSES_ROOT\Directory\Shellex\ContextMenuHandlers\
下新建一項,命名為SrcCount,它的默認鍵值是組件的GUID,這裡為:
{548773BA-874E-4C02-9DC7-B7A096772C7D}
現在在資源管理器裡對文件夾按快捷菜單,看到了嗎,多出一菜單項了:源代碼統計…,當我們單擊該項時即可進行代碼統計。
本程序主要是展示怎樣使用Shell擴展,所以重點在於程序設計方法,並未對所有細節的地方做得盡善盡美。還有一些細節值得改進,如Shell擴展菜單的美化效果(例如比較流行的軟件WinZIP、WinRAR之類的界面效果,快捷菜單上繪出位圖)還可以改進;此外,程序的功能可以進一步擴充,本文主要是對C/C++源代碼進行統計,我們可以擴展相關的統計規則,可以對匯編、JAVA、Delphi等各種語言的源代碼進行統計,還可以用對話框的形式讓用戶進行各種選擇與設置統計規則等。有興趣的朋友可以一試。
本程序雖在Windows XP、VC++6.0下編譯,但可適用於Windows 9X/NT/2000/XP, 本文簡單地簡介了Shell擴展技術的實現方法,若有語焉不詳的地方,請參考本文所附的源代碼,或者發電子郵件給我,我們一起交流。
本文配套源碼