Chinaunix首页 | 论坛 | 博客
  • 博客访问: 588555
  • 博文数量: 752
  • 博客积分: 40000
  • 博客等级: 大将
  • 技术积分: 5005
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-13 14:47
文章分类

全部博文(752)

文章存档

2011年(1)

2008年(751)

我的朋友

分类:

2008-10-13 16:41:03

用Shell扩展实现源代码统计程序


作者/




一、前言
   在 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.
--------------------next---------------------

在WinXP sp2下无效,为什么? ( buyong 发表于 2008-6-12 17:03:00)
 
怎样注册成为对任何文件和文件夹该上下文菜单都有效呀?
我向注册表HKEY_CLASSES_ROOT\AllFilesystemObjects\shellex\ContextMenuHandlers\SrcCount里添加了{548773BA-874E-4C02-9DC7-B7A096772C7D}项,尽管能对所有文件和文件夹有效,可是对桌面上的快捷方式(shortcut)有两个同样的上下文菜单,该如何解决?

谢谢 ( tangbisheng 发表于 2005-6-23 14:27:00)
 
我提两个极其有用的问题
第一,改程序只能对文件间进行操作,如何修改实现对文件进行操作。因为统计源代码对文件也可以操作的。
第二,该程序只能统计一个文件夹的程序,当用鼠标选中多个文件夹时,只能计算第一个文件家里的程序。如果要能计算多个文件夹里的程序,该如何修改程序? ( yetty 发表于 2004-8-27 11:19:00)
 
好玩,
我也试试
( Madlee 发表于 2004-1-9 8:58:00)
 
Thanks! ( xiaojin 发表于 2003-9-8 11:35:00)
 
.......................................................

--------------------next---------------------

阅读(319) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~