用Shell擴充套件實現原始碼統計程式

Just4life發表於2013-06-14
一、前言

在 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。它的作用是統計原始碼檔案的行數。下面是程式碼的主要實現部分:

01.//////////////////////////////////////////////////////////////////
02.// 作用:獲取原始檔的程式碼行數
03.// 引數:1. pFilePath :輸入引數,指定原始檔的路徑;
04.//       2. lines:輸出引數,獲得原始檔的程式碼行數。
05.STDMETHODIMP CCountLines::GetFileLines(BSTR *pFilePath, int *lines)
06.{
07.    // 存放程式碼的總行數
08.    int totalLine = 0; ?
09.    // 開啟檔案
10.    HANDLE hFile = CreateFile((TCHAR *)pFilePath, ?
11.                                GENERIC_READ,
12.                                FILE_SHARE_READ,
13.                                NULL,
14.                                OPEN_EXISTING,
15.                                FILE_ATTRIBUTE_NORMAL,
16.                                NULL);
17.    if ((HANDLE)-1 == hFile)
18.    {
19.        *lines = -1;
20.        return S_FALSE;
21.    }    
22.    // 開闢緩衝區存放檔案內容
23.    DWORD dwFileSize = GetFileSize(hFile, NULL);     
24.    BYTE *lpBuffer = new BYTE[dwFileSize];
25.    memset(lpBuffer, 0, dwFileSize); ?
26.    DWORD dwRead = 0;
27.    BOOL bReadFile = ReadFile(hFile, lpBuffer, dwFileSize, &dwRead, NULL);
28.    assert(bReadFile && dwRead == dwFileSize); ?
29.    // 我們這裡僅用一個簡單的統計規則,即以換行符(‘\n’)為一行程式碼結束的標記
30.    for (unsigned i = 0; i < dwFileSize; i++)
31.    {
32.        if (lpBuffer[i] == ''\n'')
33.        {
34.            totalLine++;
35.        }
36.    }  
37.    // 釋放緩衝區
38.    delete []lpBuffer;
39.    CloseHandle(hFile);
40.    // 儲存程式碼行數
41.    *lines = totalLine + 1; ?
42.    return S_OK;
43.}

6.繼續新增方法GetFolderLines,它將根據遞迴演算法對資料夾裡的每個檔案進行程式碼統計,這裡就不具體寫出了,請參看原始碼。

7.在CCountLines的基類中新增IShellExtInit、IContextMenu。

8.當瀏覽器explorer.exe載入我們的程式段時,將呼叫IShellExtInit 介面初始化選單,然後呼叫介面IContexMenu處理右鍵選單,所以我們將在DLL元件中暴露以上介面。這隻需要在BEGIN_COM_MAP()與END_COM_MAP()巨集中加入介面即可。

9.Windows視窗初始化快捷選單時呼叫IShellExtInit介面的Initialize ()方法,函式原型如下:

1.HRESULT Initialize(LPCITEMIDLIST pidlFolder,LPDATAOBJECT lpdobj, HKEY hkeyProgID );

我們將在這個函式裡進行必要的初始化動作,例如儲存檔名的完整路徑,儲存登錄檔的鍵值等。

10.瀏覽器呼叫IContexMenu介面進行命令的解釋執行,這是我們進行原始碼統計的主要部分,我們將呼叫以上的演算法對所選定的資料夾按照約定的規則進行程式碼統計。這個介面主要有以下三個方法需要實現:  

01.// 在視窗的狀態列上顯示命令說明,這裡值得注意的是,我們需要對ASCII碼和UNICODE碼進行處理,
02.// 以適應不同系統。
03.
04.HRESULT GetCommandString(
05.    UINT idCmd,
06.    UINT uFlags,
07.    UINT *pwReserved,
08.    LPSTR pszName,
09.    UINT cchMax
10.   );
11.
12.// 執行選單明命令,在此可以實現具體的功能。
13.HRESULT InvokeCommand(
14.   LPCMINVOKECOMMANDINFO pici
15.   );
16.
17.// 在這裡增加快捷選單
18.HRESULT QueryContextMenu(
19.    HMENU hmenu,
20.    UINT indexMenu,
21.    UINT idCmdFirst,
22.    UINT idCmdLast,
23.    UINT uFlags
24.);

這裡僅舉例 InvokeCommand()的實現,其他請看原始碼。

01.///////////////////////////////////////////////////////////////////
02.// 作用:執行快捷選單命令
03.// 引數:1. pici:包含命令資訊的結構體  
04.HRESULT CCountLines::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
05.{
06.    BOOL bEx = FALSE;
07.    BOOL bUnicode = FALSE;?
08.
09.    if (pici->cbSize = sizeof(CMINVOKECOMMANDINFOEX))
10.    {
11.        bEx = TRUE;
12.        if ((pici->fMask & CMIC_MASK_UNICODE))
13.        {
14.            bUnicode = TRUE;
15.        }
16.    }
17.// lpVerb引數有兩種標識:如高位字非0,則為命令字串,否則低位提供了快捷選單的偏移值。
18.    if (!bUnicode && HIWORD(pici->lpVerb))
19.    {
20.        if(StrCmpIA(pici->lpVerb, "Stat."))
21.        {
22.
23.           return E_FAIL;
24.        }
25.    }
26.
27.    else if (bUnicode && HIWORD(((CMINVOKECOMMANDINFOEX *) pici)->lpVerbW))
28.    {
29.        if(StrCmpIW(((CMINVOKECOMMANDINFOEX *)pici)->lpVerbW, L"Stat."))
30.        {
31.            return E_FAIL;
32.        }
33.    }
34.    else if (LOWORD(pici->lpVerb) != IDM_SRC_COUNT)
35.    {
36.
37.        return E_FAIL;
38.     }
39.    else
40.    {
41.                 assert(0 == HIWORD(pici->lpVerb));
42.                 int lines = 0;
43.                 TCHAR szTitle[MAX_PATH] = {0};
44.                 TCHAR szMsg[MAX_PATH] = {0};
45.                 TCHAR szFormat[MAX_PATH] = {0};
46.                 memset(szMsg, 0, MAX_PATH);
47.                 //儲存當前游標並重設為等待形狀
48.                 HCURSOR hOldCursor = GetCursor();??
49.                 HCURSOR hNewCursor = LoadCursor(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDC_COUNT_WAIT));
50.                 assert(hNewCursor);
51.                 SetCursor(hNewCursor);?
52.                 TCHAR szTemp[MAX_PATH] = {0};
53.                 LoadString(_Module.GetModuleInstance(), IDS_TOTAL_LINES, szFormat, MAX_PATH);                  
54.
55.                 if (SUCCEEDED(GetFolderLines((BSTR *)&m_pszPath, &lines)))
56.                 {
57.                          wsprintf(szMsg, szFormat, (LPTSTR)m_pszPath, lines);
58.                 }       
59.
60.                 // 恢復預設游標形狀
61.                 SetCursor(hOldCursor);
62.
63.                 // 顯示統計程式碼資訊
64.                 LoadString(_Module.GetModuleInstance(), IDS_TITLE, szTitle, MAX_PATH);
65.                 MessageBox(pici->hwnd, szMsg, szTitle, MB_OK | MB_ICONINFORMATION);
66.    }
67.    return S_OK;
68.   }

四、其它

本程式是程式內服務程式,執行regsvr32進行註冊(注:在VC編譯器中,COM元件在編譯時會自動呼叫regsvr32 進行註冊,請看工程配置檔案),例如,該元件已COPY至C:\WinNT\System32下,我們將輸入如下命令列註冊:

圖三 示例程式碼執行效果圖三

因為是對資料夾統計,所以還需在

1.HKEY_CLASSES_ROOT\Directory\Shellex\ContextMenuHandlers\

下新建一項,命名為SrcCount,它的預設鍵值是元件的GUID,這裡為:

1.{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.

相關文章