65.9K
CodeProject 正在变化。 阅读更多。
Home

CEnum - 文件枚举和文件通配符类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (5投票s)

2008 年 5 月 30 日

CPOL

12分钟阅读

viewsIcon

56263

downloadIcon

1298

CEnum 用于使用通配符匹配(通配符)枚举文件和目录。

目录

引言

CEnum 是一个快速简单的类,允许您枚举(即列出)目录中的所有文件。
它支持

  • 通配符“*”和“?”的使用(即它也是一个文件通配符类)
  • 单独枚举文件和子目录
  • 为文件和目录单独设置包含列表、排除列表和忽略大小写选项
  • 递归搜索(即它也可以枚举子目录的内容)
  • 使用文件的完整路径或仅文件名进行枚举
  • 用STL编写,但可以选择支持MFC集合类
  • 支持Unicode(在微软世界里,UNICODE代表UTF-16小端序)
  • 作为::FindFirstFile::FindNextFile的包装器

它是如何工作的?

CEnum本身就很简单

// enumerate all files on C: drive

CEnum enumerator;
enumerator.bRecursive = true;          // search subdirectories too
enumerator.bFullPath  = true;          // enumerate using file's full path

// start search
enumerator.EnumerateAll(_T("C:\\"));   // root directory to start your search from

list<string> * pAllFilesOnCDrive = enumerator.GetFiles();

// congrats, you have just created a list of all files on C: drive
// lets print it

list<string>::iterator iter = pAllFilesOnCDrive->begin();
for (; iter != pAllFilesOnCDrive->end(); ++iter)
{
    cout << iter->c_str();
}

// at this point just move on,
// CEnum will release memory allocated by pAllFilesOnCDrive for you

如果您非常讨厌STL,并且想使用MFC容器,该怎么办?
不用担心,CEnum也能满足您的需求

  • enum.cpp的顶部取消注释此行://#define MFC
  • 重新编译
// enumerate all MP3's on C: drive

CEnum enumerator;
enumerator.sIncPatternFiles  = _T("*.mp3");
enumerator.bNoCaseFiles      = true;       // enumerate both *.mp3 and *.MP3
enumerator.bRecursive        = true;
enumerator.bFullPath         = true;


// start search
enumerator.EnumerateAll(_T("C:"));         // unlike in the first example, this time
                                           // there is no backslash at the end
                                           // of path string

CStringArray * pCStringArray = enumerator.GetFilesAsCStringArray();
for(int i=0; i<pCStringArray->GetSize(); ++i)
{
    cout << (LPCTSTR) pCStringArray->GetAt[i];
}


// or if you prefer CStringList...
CStringList  * pCStringList  = enumerator.GetFilesAsCStringList();
POSITION pos = pCStringList->GetHeadPosition();
while (pos != NULL)
{
    cout << (LPCTSTR) pCStringList->GetNext(pos);
}

// once again, memory management is taken care of by CEnum.
// at this point you can just forget that pCStringArray and pCStringList even existed

请注意,为了您的方便,根文件夹的路径可以以反斜杠结尾,也可以不以反斜杠结尾!

了解这些就足以满足您基本使用CEnum的需求了,详细信息请继续阅读。

背景

一些事情启发了我写这个类

  • 我一直都在处理文件,而且总是复制代码又粘贴(而且不知何故我总是在其中遇到bug :-))
  • 我无法在Google上找到类似的类(别笑,我讨厌浏览大型源代码网站的索引)
  • Jack Handy [^] 和 Alessandro Felice Cantatore [^] 的作品
  • C#的Directory类及其GetFiles()方法

类设计

CEnum的设计考虑了以下几点:

  • 必须开箱即用(即,只需创建一个对象,调用一个方法,您就可以获得该目录中所有文件的列表)
  • 必须使用STL设计,而不是MFC
  • 必须支持通配符(通配符搜索)
  • 必须支持Unicode
  • 在比较字符串时必须能够忽略大小写
  • 必须负责内存管理,以便调用方函数不必自行分配或释放任何STL对象

这是类

用户可以通过public成员变量设置一些选项(有关每个选项的详细描述,请参见类成员部分)。

// public member variables (user options)

typedef basic_string<TCHAR> _stl_string; // basic_string<TCHAR> will compile
                                         // into std::string or std::wstring,
                                         // based on if _UNICODE was defined

class CEnum
{
public:
    _stl_string sExcPatternDirs;     // in case of conflict Exclude pattern has
                                     // precedence over Include pattern
    _stl_string sExcPatternFiles;
    _stl_string sIncPatternDirs;
    _stl_string sIncPatternFiles;

    bool bRecursive;
    bool bFullPath;
    bool bNoCaseDirs;
    bool bNoCaseFiles;
};

默认构造函数将使用最常见的值初始化变量

// default constructor

public:
    CEnum()
    {
        plstDirs            = new list<_stl_string >;   // notice the space in front
                                                        // of the right angle bracket !!!
        plstFiles           = new list<_stl_string >;

        sExcPatternDirs     = _T("");
        sExcPatternFiles    = _T("");
        sIncPatternDirs     = _T("");
        sIncPatternFiles    = _T("");

        bRecursive          = false;
        bFullPath           = false;
        bNoCaseDirs         = false;
        bNoCaseFiles        = false;
    }

如果需要,您可以在重载的构造函数中直接设置用户选项

// parameterized constructor
    CEnum
    (
        _stl_string sPath,
        _stl_string sExcludePatternDirs     = _T(""),
        _stl_string sExcludePatternFiles    = _T(""),
        _stl_string sIncludePatternDirs     = _T(""),
        _stl_string sIncludePatternFiles    = _T(""),
        bool bRecursiveSearch               = false,
        bool bUseFullPath                   = false,
        bool bIgnoreCaseDirs                = false,
        bool bIgnoreCaseFiles               = false
    )
    {
        plstDirs          = new list<_stl_string >;
        plstFiles         = new list<_stl_string >;

        sExcPatternDirs   = sExcludePatternDirs;
        sExcPatternFiles  = sExcludePatternFiles;
        sIncPatternDirs   = sIncludePatternDirs;
        sIncPatternFiles  = sIncludePatternFiles;

        bRecursive        = bRecursiveSearch;
        bFullPath         = bUseFullPath;
        bNoCaseDirs       = bIgnoreCaseDirs;
        bNoCaseFiles      = bIgnoreCaseFiles;

        EnumerateAll(sPath);
    }

CEnum可以给您的各种列表
注意:默认情况下,MFC集合类已从生成中排除。

// internal lists

    list<_stl_string > * GetDirs();
    list<_stl_string > * GetFiles();

//#define MFC

#ifdef MFC

    CStringArray * GetDirsAsCStringArray();
    CStringArray * GetFilesAsCStringArray();
    CStringList  * GetDirsAsCStringList();
    CStringList  * GetFilesAsCStringList();

#endif

实例化

为了在使用CEnum时获得更大的灵活性,提供了两种构造函数

// ... using default constructor

CEnum enumerator;

// set options
enumerator.sExcPatternDirs  = _T("Post Black album Metallica");
enumerator.sIncPatternDirs  = _T("Iron Maiden");
enumerator.sExcPatternFiles = _T("*.wma");
enumerator.sIncPatternFiles = _T("*.mp3;*.ogg");
enumerator.bRecursive       = true;
enumerator.bFullPath        = true;
enumerator.bNoCaseDirs      = true;
enumerator.bNoCaseFiles     = true;

enumerator.EnumerateAll(_T("D:\\Music"));

list<string> * pQualityMetal = enumerator.GetFiles();

... 或者用更少的代码实现相同的功能

// ... using parameterized constructor

CEnum enumerator(
                    _T("D:\\Music"),
                    _T("Post Black album Metallica"),
                    _T("*.wma"),
                    _T("Iron Maiden"),
                    _T("*.mp3;*.ogg"),
                    true,
                    true,
                    true,
                    true
                 );

list<string> * pQualityMetal = enumerator.GetFiles();

... 或者最简单的方式(如果默认值对您有效)

// ... using parameterized constructor with default values

CEnum enumerator( _T("D:\\Music") );

list<string> * pQualityMetal = enumerator.GetFiles();

类析构函数

引用我读过的第一本C++书籍,Jesse Liberty的“21天学会C++”:“如果你写了一个需要创建内存然后将其传递给调用函数的函数,请考虑更改你的接口。让调用函数分配内存,然后通过引用将其传递给你的函数。这会将所有内存管理从你的程序中移出,回到准备删除它的函数。”

为什么我没有遵循这个伟大的建议?嗯,首先,这是设计成类似于C#的Directory类(在C#中,垃圾回收器负责内存管理,因此调用方不关心内存问题)。其次,我想要一个易于使用的类。请记住,CEnum的工作就是简单地创建文件名列表。

那么,CEnum的工作原理如下:

  1. 构造函数分配两个列表
  2. 枚举过程结束后,指向两个列表的指针将返回给调用函数
  3. 列表的指针将在CEnum的析构函数中被删除

这意味着您的枚举列表的生命周期仅限于创建它的CEnum对象的生命周期!您不能将指针传递给另一个函数。如果您想这样做,您需要创建一个列表的副本,或者注释掉析构函数中的一些(或全部)删除语句。

这是一种糟糕的设计吗?可能吧,但易用性是我设计这个类时的首要考虑因素。我希望能够在一行代码中枚举目录(请参阅上面的示例),处理那些文件(例如,在屏幕上显示它们),然后忘记CEnum对象及其分配的所有列表。如果您想“正确地”做,CEnum可以很容易地适应使用客户端应用程序分配的列表,只需添加另一个构造函数并注释掉析构函数中的几行代码即可。

类成员

这是CEnumpublic成员变量列表,用户可以通过它们设置所需的选项

  • bRecursive
    描述:如果为true,则也会枚举子目录
    默认值:false
  • bFullPath
    描述:如果为true,则使用文件的完整路径枚举文件,否则列表只包含文件名
    默认值:false
  • bNoCaseDirs
    描述:如果为true,在搜索目录(仅目录)名称时将忽略大小写
    默认值:false
  • bNoCaseFiles
    描述:如果为true,在搜索文件(仅文件)名称时将忽略大小写
    默认值:false
  • sIncPatternFiles
    描述:您希望包含在搜索中的文件的匹配模式。支持通配符“*”和“?”。如果您有多个搜索模式,请用分号分隔。
    默认值:空字符串
    示例
    • "*.mp3;*.mp4"
    • "*.mp?"(与第一个示例相同)
    • "*.mp3;iron maid*;latest*"
    请注意,在包含模式的情况下,空字符串表示“枚举所有”,即包含所有内容!!

  • sExcPatternFiles
    描述:您希望从搜索中排除的文件的匹配模式。支持通配符“*”和“?”。如果您有多个搜索模式,请用分号分隔。
    默认值:空字符串
    示例
    • "*.mp3;*.mp4"
    • "*.mp?"(与第一个示例相同)
    • "*.mp3;iron maid*;latest*"
    请注意,在排除模式的情况下,空字符串表示“枚举无”,即排除无!!
    另外,在发生冲突时,排除模式优先于包含模式。
  • sIncPatternDirs与上面sIncPatternFiles相同,只是用于目录。
  • sExcPatternDirs与上面的ExcPatternFiles相同,只是用于目录。

STL列表与MFC集合类的比较

CEnum可以提供各种列表

// ... show all containers CEnum can return

CEnum enumerator(_T("C:\\"));

list<string> * pSTLlistFiles   = enumerator.GetFiles();
list<string> * pSTLlistDirs    = enumerator.GetDirs();

StringList   * pMFClistFiles   = enumerator.GetFilesAsCStringList();
StringList   * pMFClistDirs    = enumerator.GetDirsAsCStringList();

StringArray  * pMFCArrayFiles  = enumerator.GetFilesAsCStringArray();
StringArray  * pMFCArrayDirs   = enumerator.GetDirsAsCStringArray();

由于CEnum是用STL编写的,所以在执行过程中创建的两个列表是GetDirs()GetFiles()函数返回的两个列表。所有四个MFC容器(两个CStringArrays和两个CStringLists)仅在您调用转换函数(GetFilesAs...GetDirsAs...)时创建。事实上,所有与MFC相关的代码都隐藏在预处理器指令后面,并且默认是不活动的(即不编译)。如果您需要此功能,只需取消注释//#define MFC行并重新编译。

通配符比较功能

这是另一个经常需要但不容易找到的东西。两个很好的例子是Jack Handy在这里CodeProject [^] 和 Alessandro Felice Cantatone [^] 的作品。两者都是很好的例子,但各有缺点。Jack的函数简单快速,但它不允许您忽略大小写,因为STL的tolower和UNICODE不匹配,而Alessandro的函数是为IBM OS/2设计的(更不用说他那种傲慢的态度,而且我甚至还没有开始谈论他设定的某些限制。AI与string比较有什么关系?)

无论如何,这是我想到的

// ... wildcard compare function

_tsetlocale(LC_ALL, _T("");      // execute this before calling _tcsicmp

bool CompareStrings(LPCTSTR sPattern, LPCTSTR sFileName, bool bNoCase)
{
    TCHAR temp1[2] = _T("");
    TCHAR temp2[2] = _T("");
    LPCTSTR pStar  = 0;
    LPCTSTR pName  = 0;

    while(*sFileName)
    {
        switch (*sPattern)
        {
        case '?':
            ++sFileName; ++sPattern;
            continue;
        case '*':
            if (!*++sPattern) return 1;
            pStar = sPattern;
            pName = sFileName + 1;
            continue;
        default:
            if(bNoCase)
            {
                // _tcsicmp works with strings not chars !!
                *temp1 = *sFileName;
                *temp2 = *sPattern;
                if (!_tcsicmp(temp1, temp2))    // if equal
                {
                    ++sFileName;
                    ++sPattern;
                    continue;
                }
            }
            else if (*sFileName == *sPattern)   // bNoCase == false,
            {                                   // compare chars directly
                ++sFileName;
                ++sPattern;
                continue;
            }

            // chars are NOT equal,
            // if there was no '*' thus far, strings don't match
            if(!pStar) return 0;

            // go back to last '*' character
            sPattern = pStar;
            sFileName = pName++;
            continue;
        }
    }
    // check is there anything left after last '*'
    while (*sPattern == '*') ++sPattern;
    return (!*sPattern);
}

这应该很容易理解

  • 如果在模式字符串中找到“?”,则字符匹配,函数将移到模式字符串和搜索字符串中的下一个字符。
  • 如果在模式字符串中找到“*”,则函数仅移到模式字符串中的下一个字符。如果模式字符串中没有更多字符,则退出;或者记录当前位置(“*”之后的下一个字符)和搜索字符串中下一个字符的记录。
  • 当需要忽略大小写比较两个字符时,使用运行时库_tcsicmp
    • 如果字符串(仅包含一个字符的临时字符串)相等,函数将移到模式字符串和搜索字符串中的下一个字符。
    • 如果字符串不同,并且到目前为止模式字符串中没有“*”字符,函数将返回false。否则,它将回到最后一个“*”字符的位置,并在搜索字符串中向前移动一个字符。

关于_tcsicmp的使用
我选择使用此函数没有特别的原因,只是因为它是我通过所有测试的唯一一个运行时库例程,可以忽略大小写地进行UNICODE字符串比较。

注意:我的测试仅限于欧洲字符集(例如带重音的、中欧和北欧字符)。对于更广泛的范围,您需要自行测试。

如果您想在其他项目中 private 使用通配符比较功能,并且不需要忽略大小写,那么您可以使用类似以下的方法大大加快速度:

// ... wildcard compare function without Ignore case functionality

    bool CompareStrings(LPCTSTR sPattern, LPCTSTR sFileName)
    {
        LPCTSTR pStar  = 0;
        LPCTSTR pName  = 0;

        while(*sFileName)
        {
            switch (*sPattern)
            {
            case '?':
                ++sFileName; ++sPattern;
                continue;
            case '*':
                if (!*++sPattern) return 1;
                pStar = sPattern;
                vpName = sFileName + 1;
                continue;
            default:
                if (*sFileName == *sPattern) { ++sFileName; ++sPattern; continue; }

                // chars are NOT equal,
                // if there was no '*' thus far, strings don't match
                if(!pStar) return 0;

                // go back to last '*' character
                sPattern = pStar;
                sFileName = pName++;
                continue;
            }
        }
        // check is there anything left after last '*'
        while (*sPattern == '*') ++sPattern;
        return (!*sPattern);
    }

此版本速度提高了3-4倍,因为它直接比较字符。

类似项目

如果您发现CEnum缺少某些有用的功能,请查看这些项目。它们可能更符合您的需求。

枚举和通配符

通配符匹配

演示项目

  • 演示项目中使用的CEnum有一个小区别。因为我在演示项目中添加了通配符测试功能,所以CompareStrings函数被声明为publicstatic。否则,它是CEnum的一个privatestatic方法。
  • 测试枚举
    这部分非常简单。在OnEnumeration()函数中,只需20行代码就可以枚举目录并将其内容添加到CListCtrl
  • 测试通配符比较功能
    演示使用test.txt测试文件,您可以在其中添加自己的测试用例。test.txt的格式非常简单。第一个字符串是通配符字符串,第二个字符串是搜索字符串,最后一个字符串指定前两个字符串是否匹配。注释行以“#”字符开头。

    CEnum2.png

限制

CEnum以文件为中心,这意味着它设计用于搜索文件,而不是搜索目录。
例如

  • 如果您只搜索文件只搜索目录,您将按照预期的方式进行枚举。
  • 但是,如果您对文件目录都应用过滤器(排除或包含),那么您只会枚举那些位于匹配目录过滤器的目录中的文件。

从某种角度看,后一种情况可能被视为一种限制,因为您无法独立搜索文件和目录。您无法获得所有子目录中所有文件的列表,同时又获得匹配特定搜索条件的目录列表。在这种情况下,您唯一能做的就是运行两次CEnum,一次用于文件,一次用于目录。

用户还需要注意的另一件事是排序功能。在枚举完每个目录后,CEnum会调用STL列表的sort()方法。此方法按字母顺序对文件进行排序,这可能与您的文件浏览器(例如Windows资源管理器)的排序顺序不同。

最后,请注意,CEnum是为通配符搜索(globbing)设计的。它不能根据大小、日期、文件内容或文件属性搜索文件。

勘误

在Visual Studio中可能出现的编译错误是:

  • fatal error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "stdafx.h"' to your source?

解决方案是不要使用预编译头文件

  • 在项目的“解决方案资源管理器”窗格中,右键单击项目名称,然后选择“属性”。
  • 在左侧窗格中,单击“C/C++”文件夹。
  • 单击“预编译头”节点。
  • 在右侧窗格中,选择“创建/使用预编译头”,然后选择“不使用预编译头”。

修订历史

  • 版本 1.0 发布于 2008年5月 - 初始公开发布
  • 版本 1.1 发布于 2008年11月

版本 1.1 的更改列表

文章

  • 添加了析构函数章节
  • 更新了通配符搜索算法章节
  • 添加了Doxygen文档
  • 修改了演示应用程序,使其可以通过双击打开文件

源代码

  • 删除了大部分STL对象的动态分配
  • CEnum.cpp添加了Doxygen Qt风格注释
  • 更改了CompareStrings函数中的通配符比较算法
  • 重写了Tokenize函数
  • 更新了类的析构函数
  • 验证了在Visual Studio 2005的警告级别4下,该类可以干净地编译(无错误或警告)

作者许可

此类的使用没有限制。您可以(整体或部分)使用它,无论是否获得作者许可,在任何类型的项目中,无论许可问题如何,无论是在商业项目还是开源项目中。不过,发送一封感谢电子邮件会很好。:-)

结论

好了,各位,这是一个简单的类,可以让您在不了解::FindFirstFile::FindNextFile API工作原理,甚至不了解CEnum内部工作原理的情况下枚举文件。我希望CEnum能像它对我一样,在处理文件时为您节省时间。

我非常希望收到您的反馈,无论是建设性的批评、功能请求还是错误报告。请随时发表您的评论或通过电子邮件发送。我特别感兴趣的是,如果您知道类似的类或项目。如果您知道,请告诉我可以在哪里找到它。

© . All rights reserved.