CEnum - 文件枚举和文件通配符类
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
的工作原理如下:
- 构造函数分配两个列表
- 枚举过程结束后,指向两个列表的指针将返回给调用函数
- 列表的指针将在
CEnum
的析构函数中被删除
这意味着您的枚举列表的生命周期仅限于创建它的CEnum
对象的生命周期!您不能将指针传递给另一个函数。如果您想这样做,您需要创建一个列表的副本,或者注释掉析构函数中的一些(或全部)删除语句。
这是一种糟糕的设计吗?可能吧,但易用性是我设计这个类时的首要考虑因素。我希望能够在一行代码中枚举目录(请参阅上面的示例),处理那些文件(例如,在屏幕上显示它们),然后忘记CEnum
对象及其分配的所有列表。如果您想“正确地”做,CEnum
可以很容易地适应使用客户端应用程序分配的列表,只需添加另一个构造函数并注释掉析构函数中的几行代码即可。
类成员
这是CEnum
的public
成员变量列表,用户可以通过它们设置所需的选项
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
缺少某些有用的功能,请查看这些项目。它们可能更符合您的需求。
枚举和通配符
- CFileFinder - 扩展 CFileFind MFC 类的功能
- 一个方便枚举文件夹内容的类
- 使用组合文件扩展名掩码枚举文件和文件夹
- STL风格的Win32文件名迭代 - 唯一用STL编写的
- CFileFindEx - 唯一带有排除过滤器的
- 递归列出文件类型信息和图标
- 多线程文件查找器类 - 将搜索放在单独的线程中是个不错的想法
- CFileInfoArray:
用于递归遍历目录以收集文件信息的类 - 递归模式化文件通配符
- CFileFinder - 扩展CFileFind MFC类功能 - 基于MFC,扩展了通配符功能,支持按日期和文件内容搜索
通配符匹配
- 通配符字符串比较(通配符) - Jack Handy的作品
- 使用通配符比较字符串并验证输入
- 字符串通配符匹配(* 和 ?)
- 通配符匹配算法 - Alessandro Felice Cantatore的作品
演示项目
- 演示项目中使用的
CEnum
有一个小区别。因为我在演示项目中添加了通配符测试功能,所以CompareStrings
函数被声明为public
和static
。否则,它是CEnum
的一个private
非static
方法。 - 测试枚举
这部分非常简单。在OnEnumeration()
函数中,只需20行代码就可以枚举目录并将其内容添加到CListCtrl
。 - 测试通配符比较功能
演示使用test.txt测试文件,您可以在其中添加自己的测试用例。test.txt的格式非常简单。第一个字符串是通配符字符串,第二个字符串是搜索字符串,最后一个字符串指定前两个字符串是否匹配。注释行以“#”字符开头。
限制
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
能像它对我一样,在处理文件时为您节省时间。
我非常希望收到您的反馈,无论是建设性的批评、功能请求还是错误报告。请随时发表您的评论或通过电子邮件发送。我特别感兴趣的是,如果您知道类似的类或项目。如果您知道,请告诉我可以在哪里找到它。