【例子】显示代码文件行数的资源管理器插件

xiaotong2017-08-18 07:35:39
项目列表:
下载源代码:

在这篇文章中,我将介绍怎样用VC6.0开发一个资源管理器插件,以实现通过文件夹详细资料里的列显示其中代码文件的行数。

最终实现的效果图如下:

这里显示的是文件夹C:\Pojects\lnshell里的代码文件,其中Code lines和Blank lines两列是由本插件提供的,分别显示文件的代码行和空白行。而属性页Statistics of code也由本插件提供,可以统计多个文件的汇总信息。

使用插件

用VC6.0编译文章附带的项目,在命令行调用 regsvr32 /v "生成的dll" 安装此插件。

本插件提供的信息列只有在文件夹的详细资料视图中才会显示。将你想要显示代码行的文件夹切换到详细资料视图,并通过列头的右键菜单打开列设置添加Code lines和Blank lines列。

行统计

本程序的核心功能是统计文件中的代码行数。实际上程序并没有判断文件是不是源代码文件,所以代码行实际上就是指非空行。

非空行 = 所有行 - 空行,我们只要分别算出文件中的所有行和所有空行就行了。

计算行数就是计算文件里的回车换行符。但是在这之前有一个大前提,就是文件要是文本文件,因为二进制文件里面也可能出现大量的回车换行符,但是那是八个二进制字符,而不是ascii字符。

因此逻辑上先从判断文件是否是文本文件开始。我们用猜字符的方法来进行判断。也就是文件中如果没有'\0'字符,就认为它是文本文件。如果有超过两个'\0'字符同时出现,它就肯定是二进制文件。如果一个'\0'字符和一个非'\0'字符,它有可能是Unicode文本。Unicode文本又有大端(Big Endian)和小端(Little Endian)之分,如果文件中既有大端文本又有小端文本,那它也肯定不是Unicode文件,只可能是二进制文件。

UINT GuessDocumentCodePage(LPCSTR lpszText, int cchText)
{
    if( (lpszText == NULL) || (cchText < 0) )
        return CP_UNKNOWN;
    if( (*lpszText == (char)0xFE) && (*(lpszText + 1) == (char)0xFF) )  // Big Endian
        return CP_UTF16;
    if( (*lpszText == (char)0xFF) && (*(lpszText + 1) == (char)0xFE) )  // Little Endian
        return CP_UTF16;

    const BYTE* lpbText = (const BYTE*)lpszText;
    UINT nCodePage = CP_UNKNOWN;

    for( int n = 0; n < cchText; n++ )
    {
        BYTE b = *(lpbText + n);

        if( b == '\0' )
        {
            if( (n % 2 == 0) || (n == 0) || ((*(lpbText + n - 1) == '\0') || (*(lpbText + n - 1) > 127)) )
                return CP_UNKNOWN;
            nCodePage = CP_UTF16;
        }
    }
    return (nCodePage == CP_UNKNOWN) ? CP_ACP : nCodePage;
}

经过猜字符确认是文本文件后,行统计才有意义。如果是Unicode文本文件,先对其进行转码。

bool ConvertText2ACP(LPCWSTR lpwszText, int cchText, std::string &strText)
{
    if( lpwszText == NULL )
        return false;

    LPWSTR lpwszText2 = NULL;
    LPCSTR lpszText = (LPCSTR)lpwszText;

    if( (*lpszText == (char)0xFE) && (*(lpszText + 1) == (char)0xFF) )  // Big Endian
    {
        // Convert Unicode Big Endian to Unicode.
        lpwszText2 = (LPWSTR)malloc(sizeof(wchar_t) * (cchText + 1));
        for( int nChar = 0; nChar < cchText; nChar++ )
        {
            const wchar_t& wc = lpwszText[nChar];
            wchar_t& wc2 = lpwszText2[nChar];

            wc2 = ((wc & 0xFF00) >> 8) | ((wc & 0xFF) << 8);
        }
        lpwszText2[cchText] = L'\0';
        lpwszText = lpwszText2 + 1;
        cchText--;
    }
    else if( (*lpszText == (char)0xFF) && (*(lpszText + 1) == (char)0xFE) ) // Little Endian
    {
        lpwszText++;
        cchText--;
    }

    int iTextLen;

    iTextLen = WideCharToMultiByte(CP_ACP, 0, lpwszText, cchText, NULL, 0, NULL, NULL);
    if( iTextLen > 0 )
    {
        LPSTR szText = (LPSTR)malloc(sizeof(char) * (iTextLen + 1));

        if( szText != NULL )
        {
            WideCharToMultiByte(CP_ACP, 0, lpwszText, cchText, szText, iTextLen, NULL, NULL);
            szText[iTextLen] = '\0';
            strText = szText;
            free(szText);
        }
    }
    if( lpwszText2 != NULL )
        free(lpwszText2);
    return strText.length() == cchText;
}

整个猜文件编码格式和转码封装在一个函数中。

UINT GuessAndConvert(LPCSTR lpszText, int cchText, std::string &strText)
{
    UINT nCodePage = CP_UNKNOWN;

    nCodePage = GuessDocumentCodePage(lpszText, cchText);
    if( nCodePage == CP_UTF16 )
    {
        if( !ConvertText2ACP((LPCWSTR)lpszText, cchText / 2, strText) )
            nCodePage = CP_UNKNOWN; // Not a valid Unicode text file.
    }
    else
        strText = lpszText;
    return nCodePage;
}

再和打开文件一起,封装在OpenTextFile函数中。这个函数可以直接传入一个文件路径,如果是文本文件,通过参数strText返回经过转码后(转码为CP_ACP)的文本,否则,返回CP_UNKNOWN,表示此文件无法统计行数。

UINT OpenTextFile(LPCTSTR lpszFilePath, std::string &strText, UINT nCodePage)
{
    if( (lpszFilePath != NULL) && (*lpszFilePath != _T('\0')) )
    {
        FILE* pFile = _tfopen(lpszFilePath, _T("rb"));

        if( pFile == NULL )
            return nCodePage;

        LPSTR lpszText = NULL;
        size_t iTextLen = 0;

        fseek(pFile, 0, SEEK_END);
        iTextLen = ftell(pFile);
        fseek(pFile, 0, SEEK_SET);
        lpszText = (LPSTR)malloc(iTextLen + 1);
        fread((void*)lpszText, 1, iTextLen, pFile);
        lpszText[iTextLen] = '\0';

        nCodePage = GuessAndConvert(lpszText, iTextLen, strText);
        free(lpszText);
        fclose(pFile);
        return nCodePage;
    }
    return CP_UNKNOWN;
}

有了OpenTextFile返回的文本后,就可以读取其中的回车换行符了。在我们的程序中并不直接计算回车换行符的个数,而是用函数ReadLine逐一读入每行的文本,因为还需要计算空行,以后还可以根据行内容进行扩展。

EOL ReadLine(LPCSTR lpszText, int iTextLen, std::string &strLine, int &iLineEndPos)
{
    if( lpszText == NULL )
        return EOL_NOENDING;
    if( iTextLen < 0 )
        iTextLen = strlen(lpszText);

    LPCSTR lpszLineStart = lpszText;

    for( int nChar = 0; (nChar < iTextLen) && (*lpszText != '\0'); nChar++ )
    {
        if( *lpszText == '\r' )
        {
            if( nChar + 1 < iTextLen )
            {
                if( *(lpszText + 1) == '\n' )   // \r\n
                {
                    strLine.assign(lpszLineStart, nChar);
                    nChar++;
                    iLineEndPos = nChar + 1;
                    return EOL_CRLF;
                }
            }
            strLine.assign(lpszLineStart, nChar);
            iLineEndPos = nChar + 1;
            return EOL_CR;
        }
        else if( *lpszText == '\n' )
        {
            strLine.assign(lpszLineStart, nChar);
            iLineEndPos = nChar + 1;
            return EOL_LF;
        }
        lpszText++;
    }
    strLine.assign(lpszLineStart, nChar);
    iLineEndPos = nChar;
    return EOL_NOENDING;
}

在ReadLine中,根据不同文本格式,可分别读入Windows格式的行,Unix格式的行,MAC格式的行,它们分别以\r\n,\n,\r作为行结束符。也就是说,如果读入的是\r\n,仅返回一行,而读入的是\n\r,ReadLine会返回两次,第二次是以\r结尾(MAC标准格式)的一个空行。

最后把ReadLine返回的行进行计算,根据其内容分为空行和非空行。因为是代码文件,把只有空格和tab符的行也当作空行。

void CountLines(LPCSTR lpszText, int &cTotalLines, int &cBlankLines)
{
    if( lpszText == NULL )
        return;

    LPCSTR lpszLineStart = lpszText;
    int iTextLen = strlen(lpszText);
    int iLineEndPos = 0;
    EOL eLineEnding = EOL_NOENDING;

    do {
        std::string strLine;

        eLineEnding = ReadLine(lpszLineStart, iTextLen, strLine, iLineEndPos);
        cTotalLines++;
        if( strLine.empty() )
            cBlankLines++;
        else
        {
            for( int nChar = 0; nChar < strLine.length(); nChar++ )
            {
                if( (strLine[nChar] != ' ') && (strLine[nChar] != '\t') )
                    break;
            }
            if( nChar >= strLine.length() )
                cBlankLines++;
        }
        iTextLen -= iLineEndPos;
        lpszLineStart += iLineEndPos;
    }while( eLineEnding != EOL_NOENDING );
}

这就是本插件中行统计部分的整个过程。

集成到插件中

编写一个资源管理器的插件需要提供插件的注册信息,以及用ATL实现一个COM组件,它必须实现IShellExtInit接口,以及根据需要实现资源管理器不同部分的接口。

我们的插件提供了详细资料里的列,因此要在注册表中注册ColumnHandlers。

    NoRemove Folder
    {
        NoRemove Shellex
        {
            NoRemove ColumnHandlers
            {
                ForceRemove {62EB09EF-8A99-4CAA-9ABE-D35556104F22}
            }
        }
    }

以及实现IColumnProvider接口。

    public IColumnProvider
    COM_INTERFACE_ENTRY(IColumnProvider)
// IColumnProvider 
public:
    STDMETHOD (Initialize)(LPCSHCOLUMNINIT psci) { return S_OK; } 
    STDMETHOD (GetColumnInfo)(DWORD dwIndex, SHCOLUMNINFO* psci); 
    STDMETHOD (GetItemData)(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData); 

资源管理器通过此接口的GetColumnInfo获知此插件提供多少列以及每列的名称。

我们这里提供两列,第一列叫做Code lines。

    case 0:
        psci->scid.fmtid = CLSID_LineNumberShell; // Use our CLSID as the format ID
        psci->scid.pid   = 0;                   // Use the column # as the ID
        psci->vt         = VT_I4;               // We'll return the data as a string
        psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
        psci->csFlags    = SHCOLSTATE_TYPE_INT; // Data should be sorted as strings
        psci->cChars     = 12;                  // Default col width in chars
        LoadString(_Module.GetResourceInstance(), IDS_CODE_LINES, szSignColumn, 256);
        lstrcpynW(psci->wszTitle, T2W(szSignColumn), MAX_COLUMN_NAME_LEN);
        break;

第二列叫做Blank lines。

    case 1:
        psci->scid.fmtid = CLSID_LineNumberShell; // Use our CLSID as the format ID
        psci->scid.pid   = 1;                   // Use the column # as the ID
        psci->vt         = VT_I4;               // We'll return the data as a string
        psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
        psci->csFlags    = SHCOLSTATE_TYPE_INT; // Data should be sorted as strings
        psci->cChars     = 12;                  // Default col width in chars
        LoadString(_Module.GetResourceInstance(), IDS_BLANK_LINES, szSignColumn, 256);
        lstrcpynW(psci->wszTitle, T2W(szSignColumn), MAX_COLUMN_NAME_LEN);
        break;

资源管理器通过此接口的GetItemData获取文件每列的值。我们这里只要打开文件,如果是文本文件就计算行,返回行数给资源管理器就行了。

        if( OpenTextFile(W2CT(pscd->wszFile), strText, CP_UNKNOWN) == CP_UNKNOWN )
            return S_FALSE;

        int cTotalLines = 0;
        int cBlankLines = 0;

        CountLines(strText.c_str(), cTotalLines, cBlankLines);

这样我们的插件就给文件夹的详细资料视图提供了显示里面代码文件的代码行和空行的信息列。

我们的插件还提供了一个属性页,可以统计多个文件的汇总信息。

首先要在注册表中注册PropertySheetHandlers。

    NoRemove *
    {
        NoRemove shellex
        {
            NoRemove PropertySheetHandlers
            {
                {62EB09EF-8A99-4CAA-9ABE-D35556104F22}
            }
        }
    }

以及实现IShellPropSheetExt接口。

    public IShellPropSheetExt,
    COM_INTERFACE_ENTRY(IShellPropSheetExt)
// IShellPropSheetExt
public:
    STDMETHOD (AddPages)(LPFNADDPROPSHEETPAGE, LPARAM); 
    STDMETHOD (ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM) { return E_NOTIMPL; }

资源管理器通过这个接口的AddPages获取此插件提供的属性页。

我们通过此函数向资源管理器添加一个名为Statistics of code的属性页。

因为我们打开此属性页首先需要在资源管理器中选中一个或多个文件,这些文件的文件路径会通过IShellExtInit::Initialize函数传递给本插件。

我们把这些路径字符串转储到插件类的m_vstrFiles成员变量中,这是一个字符串数组。

    for( int nFile = 0; nFile < uNumFiles; nFile++ )
    {
        TCHAR szFile[MAX_PATH];

        DragQueryFile(hDrop, nFile, szFile, MAX_PATH);
        m_vstrFiles.push_back(szFile);
    }

接下来再看IShellPropSheetExt的AddPages函数。它把m_vstrFiles文件路径字符串数组复制一份。

    std::vector* pvstrFiles = new std::vector(m_vstrFiles);

然后作为对话框消息函数的参数传递给对话框消息处理函数。

    psp.pfnDlgProc = PropPageDlgProc;
    psp.lParam = (LPARAM)pvstrFiles;

在属性页中就知道是哪些文件的属性了。在我们的插件中,对这些文件做行统计,并将汇总信息显示在属性页上。

            std::vector* pvstrFiles = (std::vector*) ppsp->lParam;
            int cCodeFiles = 0;
            int cTotalLines = 0;
            int cBlankLines = 0;

            if( pvstrFiles != NULL )
            {
                for( int nFile = 0; nFile < pvstrFiles->size(); nFile++ )
                {
                    std::tstring& strFile = (*pvstrFiles)[nFile];
                    std::string strText;

                    if( OpenTextFile(strFile.c_str(), strText, CP_UNKNOWN) != CP_UNKNOWN )
                    {
                        cCodeFiles++;
                        CountLines(strText.c_str(), cTotalLines, cBlankLines);
                    }
                }
                delete pvstrFiles;
            }
            TCHAR szInformationFormat[256];
            TCHAR szInformation[300];

            LoadString(_Module.GetResourceInstance(), IDS_TOTAL_FILES_STATIC, szInformationFormat, 256);
            _stprintf(szInformation, szInformationFormat, pvstrFiles->size(), cCodeFiles);
            SetWindowText(GetDlgItem(hwnd, IDC_TOTAL_FILES), szInformation);
            LoadString(_Module.GetResourceInstance(), IDS_TOTAL_LINES_STATIC, szInformationFormat, 256);
            _stprintf(szInformation, szInformationFormat, cTotalLines);
            SetWindowText(GetDlgItem(hwnd, IDC_TOTAL_LINES), szInformation);
            LoadString(_Module.GetResourceInstance(), IDS_CODE_LINES_STATIC, szInformationFormat, 256);
            _stprintf(szInformation, szInformationFormat, cTotalLines - cBlankLines);
            SetWindowText(GetDlgItem(hwnd, IDC_CODE_LINES), szInformation);
            LoadString(_Module.GetResourceInstance(), IDS_BLANK_LINES_STATIC, szInformationFormat, 256);
            _stprintf(szInformation, szInformationFormat, cBlankLines);
            SetWindowText(GetDlgItem(hwnd, IDC_BLANK_LINES), szInformation);

这样我们的插件就给多个文件提供了代码行和空行的汇总信息属性页。