用Shell擴充實現原始碼統計程式
作者/king_dxs
下載原始碼
一、前言
在 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擴充技術的實現方法,若有語焉不詳的地方,請參考本文所附的原始碼,或者發電子郵件給我,我們一起交流。
五、參考資料
1. MSDN, Microsoft Corp.