CFileInfoArray: 用于递归遍历目录以收集文件信息的类






4.81/5 (19投票s)
1999年11月24日
10分钟阅读

163529

7207
此类通过目录递归收集文件信息,并额外计算32位文件校验和和CRC。
摘要
我曾参与过一些需要通过目录收集文件的项目,而这个类正是为此而设计的:通过目录递归收集文件信息,并额外计算32位文件校验和(注意这不是NT的可执行文件校验和,后者使用MapFileAndChecksum
计算)和32位文件CRC(使用了借鉴的代码,我不想重复造轮子,否则就得翻阅我的《编码理论》笔记,而我有点对灰尘过敏)。
本文的第二部分介绍了FCompare
,这是一个CFileInfo
和CFileInfoArray
的示例应用。此应用程序执行
- 给定目录和文件掩码,递归搜索源文件和目标文件进行比较。
- 通过文件大小、部分/全部内容、部分/全部校验和或部分/全部CRC进行源文件与目标文件的二进制比较。
- 将匹配的文件名和路径填充到列表视图中。
构建环境
VC++ 6.0,警告级别4。已在Windows NT 4.0和W'95上测试。
尽管未测试,但我猜
CFileInfo
、CFileInfoArray
和FCompare
可以安全地重新编译为Unicode。CFileInfo 和 CFileInfoArray
/**
* @class Stores information about a file in a
* way like CFindFile does
*/
class CFileInfo {
public:
/** @access Public members */
CFileInfo();
/**
* @cmember Copy constructor
* @parm CFileInfo to copy member variables from.
*/
CFileInfo(const CFileInfo& finf);
/**
* @cmember Destructor
*/
~CFileInfo();
/**
* @cmember Initializes CFileInfo member variables.
* @parm Values to init member variables.
* @parm Path of the file the CFileInfo refers to.
* @parmopt User defined parameter.
*/
void Create(const WIN32_FIND_DATA* pwfd, const CString strPath,
LPARAM lParam=NULL);
/**
* @cmember Initializes CFileInfo member variables.
* @parm Absolute path for file or directory
* @parmopt User defined parameter.
*/
void Create(const CString strFilePath, LPARAM lParam = NULL);
/**
* @cmember Calcs 32bit checksum of file (i.e. sum of
* all the DWORDS of the file,
* truncated to 32bit).
* @parmopt Number of maximum bytes read for checksum calculation.
* This number is up-rounded to a multiple of 4 bytes (DWORD).
* If 0 or bigger than uhFileSize,
* checksum for all the file is calculated.
* @parmopt Force recalculation of checksum (otherwise if
* checksum has already been calculated,
* it isn't calculated again and previous calculated value is
* returned).
* @parmopt Flag to allow calling application to abort the
* calculation of checksum (for multithreaded applications).
* @parmopt Pointer to counter of bytes whose checksum
* has been calculated.
* This value is updated while checksum is being calculated,
* so calling application
* can view the progress of checksum calc (for
* multithreaded applications).
* Maximum value for pulCount is uhFileSize.
*/
DWORD GetChecksum(const ULONGLONG uhUpto=0, const BOOL bRecalc = FALSE,
const volatile BOOL* pbAbort=NULL, volatile ULONG* pulCount = NULL);
/**
* @cmember Calcs 32bit CRC of file contents
* (i.e. CRC of all the DWORDS of the file).
* @parmopt Number of maximum bytes read for CRC calculation.
* This number is up-rounded to a multiple of 4 bytes (DWORD).
* If 0 or bigger than uhFileSize, CRC
* for all the file is calculated.
* @parmopt Force recalculation of CRC (otherwise if CRC has already
* been calculated, it isn't calculated again and previous
* calculated value is returned).
* @parmopt pbAbort Flag to allow calling application
* to abort the calculation of
* CRC (for multithreaded applications).
* @parmopt Pointer to counter of bytes whose CRC has been calculated.
* This value is updated while CRC is being calculated,
* so calling application
* can view the progress of CRC calc (for multithreaded applications).
* Maximum value for pulCount is uhFileSize.
*/
DWORD GetCRC(const ULONGLONG dhUpto=0, const BOOL bRecalc = FALSE,
const volatile BOOL* pbAbort=NULL, volatile ULONG* pulCount = NULL);
/** @cmember File size in bytes as a DWORD value. */
DWORD GetLength(void) const { return (DWORD) m_uhFileSize; };
/** @cmember File size in bytes as an ULONGLONG value. */
ULONGLONG GetLength64(void) const { return m_uhFileSize; };
/** Get File split info (equivalent to CFindFile members) */
/**
* @cmember Gets the file drive
* @rdesc Returns C: for C:\WINDOWS\WIN.INI
*/
CString GetFileDrive(void) const;
/**
* @cmember Gets the file dir
* @rdesc Returns \WINDOWS\ for C:\WINDOWS\WIN.INI
*/
CString GetFileDir(void) const;
/** @cmember returns WIN for C:\WINDOWS\WIN.INI */
CString GetFileTitle(void) const;
/** @cmember returns INI for C:\WINDOWS\WIN.INI */
CString GetFileExt(void) const;
/** @cmember returns C:\WINDOWS\ for C:\WINDOWS\WIN.INI */
CString GetFileRoot(void) const { return GetFileDrive() + GetFileDir(); };
/** @cmember returns WIN.INI for C:\WINDOWS\WIN.INI */
CString GetFileName(void) const { return GetFileTitle() + GetFileExt(); };
/** @cmember returns C:\WINDOWS\WIN.INI for C:\WINDOWS\WIN.INI */
const CString& GetFilePath(void) const { return m_strFilePath; }
/* Get File times info (equivalent to CFindFile members) */
/** @cmember returns creation time */
const CTime& GetCreationTime(void) const { return m_timCreation; };
/** @cmember returns last access time */
const CTime& GetLastAccessTime(void) const { return m_timLastAccess; };
/** @cmember returns las write time */
const CTime& GetLastWriteTime(void) const { return m_timLastWrite; };
/* Get File attributes info (equivalent to CFindFile members) */
/** @cmember returns file attributes */
DWORD GetAttributes(void) const { return m_dwAttributes; };
/** @cmember returns TRUE if the file is a directory */
BOOL IsDirectory(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_DIRECTORY; };
/** @cmember Returns TRUE if the file has archive bit set */
BOOL IsArchived(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_ARCHIVE; };
/** @cmember Returns TRUE if the file is read-only */
BOOL IsReadOnly(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_READONLY; };
/** @cmember Returns TRUE if the file is compressed */
BOOL IsCompressed(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_COMPRESSED; };
/** @cmember Returns TRUE if the file is a system file */
BOOL IsSystem(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_SYSTEM; };
/** @cmember Returns TRUE if the file is hidden */
BOOL IsHidden(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_HIDDEN; };
/** @cmember Returns TRUE if the file is temporary */
BOOL IsTemporary(void) const {
return m_dwAttributes & FILE_ATTRIBUTE_TEMPORARY; };
/** @cmember Returns TRUE if the file is a normal file */
BOOL IsNormal(void) const { return m_dwAttributes == 0; };
LPARAM m_lParam; /** User-defined parameter */
private:
/** @access Private members */
CString m_strFilePath; /** @cmember Full filepath of
file (directory+filename) */
DWORD m_dwAttributes; /** @cmember File attributes of
file (as returned by FindFile() */
ULONGLONG m_uhFileSize; /** @cmember File of size.
(COM states LONGLONG as hyper,
so "uh" means unsigned hyper) */
CTime m_timCreation; /** @cmember Creation time */
CTime m_timLastAccess; /** @cmember Last Access time */
CTime m_timLastWrite; /** @cmember Last write time */
DWORD m_dwChecksum; /** @cmember Checksum calculated for the first
m_uhChecksumBytes bytes */
DWORD m_dwCRC; /** @cmember CRC calculated for
the first m_uhCRCBytes bytes */
DWORD m_uhCRCBytes; /** @cmember Number of file bytes with
CRC calc'ed (4 multiple or filesize ) */
DWORD m_uhChecksumBytes;/** @cmember Number of file bytes with
Checksum calc'ed (4 multiple or filesize) */
};
/**
* @class Allows to retrieve <c CFileInfo>s
from files/directories in a directory
*/
class CFileInfoArray : public CArray<CFILEINFO, CFileInfo&> {
public:
/** @access Public members */
/**
* @cmember Default constructor
*/
CFileInfoArray();
/**
* @cmember,menum Default values for <md CFileInfoArray.lAddParam>
*/
enum {
AP_NOSORT=0, /** @@emem Insert <c CFileInfo>s in
a unordered manner */
AP_SORTASCENDING=0, /** @@emem Insert <c CFileInfo>s in
a ascending order */
AP_SORTDESCENDING=1, /** @@emem Insert <c CFileInfo>s in
a descending number */
AP_SORTBYSIZE=2, /** @@emem AP_SORTBYSIZE | Insert
<c CFileInfo>s ordered
by uhFileSize (presumes array is
previously ordered by uhFileSize). */
AP_SORTBYNAME=4 /** @@emem AP_SORTBYNAME | Insert
<c CFileInfo>s ordered
by strFilePath (presumes array is
previously ordered by strFilePath) */
};
/**
* @cmember Adds a file or all contained in
* a directory to the CFileInfoArray
* Only "static" data for CFileInfo is filled
* (by default CRC and checksum are
* NOT calculated when inserting CFileInfos).
Returns the number of <c CFileInfo>s added to the array
* @parm Name of the directory, ended in backslash.
* @parm Mask of files to add in case that strDirName is a directory
* @parm Wether to recurse or not subdirectories
* @parmopt Parameter to pass to protected member function AddFileInfo
* @parmopt Wether to add or not CFileInfos for directories
* @parmopt Pointer to a variable to signal abort of directory retrieval
* (multithreaded apps).
* @parmopt pulCount Pointer to a variable incremented each time a CFileInfo
* is added to the array (multithreaded apps).
* @xref <mf CFileInfoArray.AddFile> <mf CFileInfoArray.AddFileInfo>
* <md CFileInfoArray.AP_NOSORT>
*/
int AddDir(const CString strDirName, const CString strMask,
const BOOL bRecurse,
LPARAM lAddParam=AP_NOSORT, const BOOL bIncludeDirs=FALSE,
const volatile BOOL* pbAbort = NULL, volatile ULONG* pulCount = NULL);
/**
* @cmember Adds a single file or directory to
* the CFileInfoArray. In case of
* directory, files contained in the directory are NOT added to the array.
* Returns the position in the array where the <c CFileInfo> was added (-1 if
CFileInfo wasn't added)
* @parm Name of the file or directory to add. NOT ended with backslash.
* @parm Parameter to pass to protected member function AddFileInfo.
* @xref <mf CFileInfoArray.AddDir> <mf CFileInfoArray.AddFileInfo>
*/
int AddFile(const CString strFilePath, LPARAM lAddParam);
protected:
/** @access Protected Members */
/**
* @cmember Called by AddXXXX to add a CFileInfo to the array.
* Can be overriden to:
* 1. Add only desired CFileInfos (filter)
* 2. Fill user param lParam
* 3. Change sort order/criteria
* Returns the position in the array where the CFileInfo was
* added or -1 if the CFileInfo
* wasn't added to the array.
* Default implementation sorts by lAddParam values and adds all CFileInfos
* (no filtering)
* @parm CFileInfo to insert in the array.
* @parm Parameter passed from AddDir function.
* @xref <mf CFileInfoArray.AddDir>
*/
virtual int AddFileInfo(CFileInfo& finf, LPARAM lAddParam);
};
如何使用
我建议您仔细阅读上面的类头文件,以全面了解这些类及其方法。供进一步参考,您可以查看FCompare的源代码(参见本文后半部分)。
无论如何,这里有一些示例代码
此代码将根目录及其子目录中的所有文件(但不包括目录本身)添加到数组中,并使用TRACE
输出它们。
CFileInfoArray fia; fia.AddDir( "C:\\", // Directory "*.*", // Filemask (all files) TRUE, // Recurse subdirs fia::AP_SORTBYNAME | fia::AP_SORTASCENDING, // Sort by name and ascending FALSE // Do not add array entries for // directories (only for files) ); TRACE("Dumping directory contents\n"); for (int i=0;i<fia.GetSize();i++) TRACE(fia[i].GetFilePath()+"\n");您也可以多次调用
AddDir
。该示例显示了C:\\和D:\\根目录中的文件(但不包括子目录)。CFileInfoArray fia; // Note both AddDir use the same sorting order and direction fia.AddDir("C:\\", "*.*", FALSE, fia::AP_SORTBYNAME | fia::AP_SORTASCENDING, FALSE ); fia.AddDir("D:\\", "*.*", FALSE, fia::AP_SORTBYNAME | fia::AP_SORTASCENDING, FALSE ); TRACE("Dumping directory contents for C:\\ and D:\\ \n"); for (int i=0;i<fia.GetSize();i++) TRACE(fia[i].GetFilePath()+"\n");
或者您可以添加单个文件
CFileInfoArray fin; // Note both AddDir and AddFile must use the // same sorting order and direction fia.AddDir("C:\\WINDOWS\\", "*.*", FALSE, fia::AP_SORTBYNAME | fia::AP_SORTDESCENDING, FALSE ); fia.AddFile("C:\\AUTOEXEC.BAT", fia::AP_SORTBYNAME | fia::SORTDESCENDING); TRACE( "Dumping directory contents for C:\\WINDOWS\\ and file C:\\AUTOEXEC.BAT\n"); for (int i=0;i<fia.GetSize();i++) TRACE(fia[i].GetFilePath()+"\n");
混合目录和单个文件
CFileInfoArray fin; // Note both AddDir and AddFile must use the same // sorting order and direction // Note also the list of filemasks *.EXE and *.COM fia.AddDir("C:\\WINDOWS\\", "*.EXE;*.COM", FALSE, fia::AP_SORTBYNAME | fia::AP_SORTDESCENDING, FALSE ); fia.AddFile("C:\\AUTOEXEC.BAT", fia::AP_SORTBYNAME | fia::SORTDESCENDING); // Note no trailing bar for next AddFile (we want to // insert an entry for the directory // itself, not for the files inside the directory) fia.AddFile("C:\\PROGRAM FILES", fia::AP_SORTBYNAME | fia::SORTDESCENDING); TRACE( "Dumping directory contents for C:\\WINDOWS\\, file C:\\AUTOEXEC.BAT and " " directory \"C:\\PROGRAM FILES\" \n"); for (int i=0;i<fia.GetSize();i++) TRACE(fia[i].GetFilePath+"\n");
实现细节和原理
- 我本可以将
CFileInfo
设计为CFindFile
的派生类,但我完全不喜欢它的FindFile
、FindNextFile
和Close
方法(我不需要它们),而且CFindFile
将信息存储为指针,这一点我也不喜欢(请参阅下面的“是指针还是不是指针”讨论,关于是否为CArray
的内容使用指向元素还是元素本身的指针)。 - 我希望它能在某种程度上兼容win64,所以我使用了Win32 API文件访问函数(在计算校验和和CRC时),这些函数可以寻址高达64位的非常大的文件。我研究了内存映射的可能性,但我认为不值得为此付出努力(欢迎志愿者)。
- Windows似乎不会缓冲API文件访问函数(至少不像
fread
那样),所以我编写了一些额外的代码在文件读取循环中,以便进行一些缓冲访问。 - 为了存储文件大小,我使用了
ULONGLONG
类型,这是一个MS专有的unsigned long long
(64位),而不是API的nFileSizeHigh
和nFileSizeLow
方案。顺便说一句,Visual C++ 6.0不支持unsigned long long
类型(尽管它为此目的定义了ULONGLONG
)。 - 我希望代码能够被中断、是线程安全的并且能够报告进度。一些校验和/CRC计算和目录检索可能会非常耗时。在中断任何可中断函数后,存储的值都是正确的,尽管可能不完整。
- 如果
AddDir
被中断,将缺少一些CFileInfos
,但数组中包含的所有CFileInfos
都是完整的。 - 如果
GetCRC
或GetChecksum
被中断,CRC或校验和将不会完全计算,并将返回中断时计算出的正确值。
- 如果
- 您可以在
AddDir
定义中看到很多volatile
限定符。这是因为这些参数将在多线程应用程序中设置,它们由AddDir
循环读取,并由另一个线程设置,因此不能缓存在寄存器中。 - 我不认为使用任何类型的安全访问公共多线程变量(如
InterlockedIncrement
等)是严格必需的:只需不过分依赖临时的奇怪的pulCount
值,但为了正确起见,我在CFileInfoArray::AddDir
、CFileInfo::GetCRC
和CFileInfo::GetChecksum
中使用了InterlockedExchange
和InterlockedIncrement
来增加pulCount
。 - 由于
volatile
限定符,主应用程序不需要使用线程安全函数(InterlockedIncrement
...)来修改多线程变量:应用程序需要修改的唯一此类变量是pbAbort
,由于其布尔性质,它不易因其非原子修改而出错。 - 在使用MFC的数组模板类时,我总是会仔细考虑是在数组中存储指向元素的指针还是元素本身。这次我决定存储元素而不是指针,因为内存分配会产生开销:递归收集文件(至少在我满盘的硬盘上)通常涉及分配数千个
CFileInfo
结构。
在数组中存储元素可以减少内存碎片,并且通过适当的CArray
重新分配增量,它还可以减少内存分配例程的调用次数(至少远非必要的一对一分配调用)。
它也有一些不便之处,例如在切换元素位置进行排序或在数组中间插入元素时:移动指针通常比移动结构快。当使用元素而不是指针时,另一个需要注意的问题是,当您通过指针外部引用元素(例如通过列表视图项的lParam
,如FCompare应用程序中那样)并向数组添加新元素时,这些引用就不再是最新的了,您必须以某种方式更新它们(在FCompare中,我通过重建列表视图来做到这一点)。 CArray
存储元素而不是存储指针的另一个好处是,当数组被释放时,元素会被自动释放。存储指针时,需要编写一个模板函数DeleteItems
来在数组中移除单个元素时释放它们。
示例应用程序:FCompare
下载源代码 - 35 KB FCompare或Binary File Compare是一个应用程序,用于二进制比较一组文件,这些文件可从给定目录和文件掩码中递归选择。
二进制比较可以通过比较文件的大小、CRC、校验和或内容来完成。在按CRC、校验和和内容进行比较时,您可以限制用于计算的字节数(从而加快计算速度)。
技术特性
- MFC对话框风格的多线程应用程序。
- 线程可以随时中止。
- 通过
WM_TIMER
消息报告线程进度,尽可能将工作线程与UI任务隔离开,并避免使它们过载(图形信息以恒定的时间速率显示和更新,而不是以工作线程的循环速率)。 - 锁定列表视图以加快元素插入速度并避免在插入元素时连续重绘。
- 使用
CTabCtrl
。 - 使用LPSTR_TEXTCALLBACK列表视图项。
如何使用
我认为使用起来非常简单,无论如何,以下是正常的使用流程- 在“目录”编辑框中填入目录,方法是键入目录或通过按
时出现的浏览目录对话框选择。如果您想递归搜索子目录,请勾选“递归目录”复选框。
- 在“文件掩码”编辑框中填入分号分隔的文件掩码列表,例如
*.htm;*.html;*.shtml;*.asp
以查找所有HTML相关文件。 - 按“添加到源”按钮。所选目录中的文件将被收集,并填充“源文件”列表视图。
- 选择另一个(或相同的)目录和文件掩码。
- 按“添加到目标”按钮。所选目录中的文件将被收集,并填充“目标文件”列表视图。
- 选择一种比较方法
- 按“大小”:大小相等的文件将匹配。
- 按“校验和”:校验和相等的文件将匹配。
- 按“CRC”:CRC相等的文件将匹配。
- 按“内容”:内容相等(逐字节)的文件将匹配。
- 如果您想抑制重复文件(出现在目标和源列表视图中的文件)在匹配列表视图中出现,请取消选中“比较重复文件”。
- 按“比较”按钮。
- 匹配的文件将出现在“比较”选项卡中。您可以通过按“导出...”按钮并选择一个文件来将三个列表导出到文件。
实现细节和原理
- 使用嵌入在更大对话框中的属性表(
CPropertySheet
)是一件很痛苦的事情- 您无法像在MS的对话框编辑器中那样编辑属性表,因此在属性表中放置比典型的三四个重叠属性页更多的控件是一场冒险。
- 即使您可以使用对话框编辑器编辑属性表,编辑属性页的方式(创建独立对话框)也很令人困惑,至少对我来说是这样,因为您无法弄清楚该页面将如何融入其父对话框的整体和谐中。
CProperty
的缘故。 - AppWizard生成的对话框应用程序不会触发
OnIdle
或WM_ENTER_IDLE
消息。此外,PumpMessage
也无法正常工作。因此,我找到的唯一一种进行进度报告循环(即使工作正在由工作线程完成)的方法是设置一个计时器。
理想的方法是使用OnIdle
或类似的基于PeekMessage
/PumpMessage
对的技巧,但如前所述,它们对对话框应用程序不起作用(或者至少我无法让它们工作)。 - “内容”匹配选项不是“win64友好”的,即使用ANSI C的
fread
,它无法寻址64位大小的文件。 - 我使用了一个精巧的文件比较算法,将O(n^2)的“常规操作”转换为O(n)(确切地说是2n)。乍一看,最明显的文件比较算法是将每个源文件与每个目标文件进行比较,这是O(n^2)。
由于我的数组已按文件大小升序排序,我可以将其转换为O(n)- 设iSource和iTarget分别为source[]和target[]数组的索引。
- iSource = 0, iTarget=0
- 当target[]和source[]都有元素时,循环执行
- 如果 target[iTarget] = source[iSource],则可能匹配,进行进一步的比较(通过校验和或其他方式)(如果您查看FCompares的源代码,您会在这里看到一些精巧的代码,以确保进行所有必需的比较)。如果进一步比较为真,则添加到匹配数组。
- 如果 target[iTarget]>=source[iSource],则 iSource++,否则 iTarget++
- 结束循环
顺便说一句,我并没有说这个算法是计算机科学的顶峰之作,我只是说它很巧妙(而且我猜它出现在Knuth的《计算机程序设计艺术》系列丛书的某个地方)。
历史
- 1999-9-23 ATL (v1.4)
-
- 根据Róbert Szucs的建议,再次修正了
GetCRC
和GetChecksum
中的一个错误。
在计算填充掩码时,应该使用4-(dwRead & 0x3)
而不是dwRead & 0x3
。
- 根据Róbert Szucs的建议,再次修正了
- 1999-9-16 ATL (v1.3)
-
- 根据Róbert Szucs的建议,修正了
GetCRC
和GetChecksum
中的一个错误。
存在一个缓冲区溢出,导致计算了最后一个双字+1的校验和和CRC,而不是最后一个双字的。访问buffer[dwRead +3...]
本应访问buffer[dwRead...]
(惭愧!:'().
- 根据Róbert Szucs的建议,修正了
- 1999-9-2 ATL (v1.2)
-
- 根据Nhycoh的建议,修正了
Create(CString, LPARAM)
中的一个错误。
在CFileInfo::Create(strFilePath, lparam)
中存在一些奇怪的代码,它使用了strFilePath.GetLength()-nBarPos
而不是nBarPos+1
(我很确定我在写那段代码的那天把脑子留在了枕头上 %-#)。 - 更新了
GetCRC
&GetChecksum
以避免一些错误情况。
- 根据Nhycoh的建议,修正了
- 1999-4-30 ATL (v1.1, 内部发布)
-
- 修正了设置计时器时的一个错误:请求的计时器id是0,而MSDN帮助指出计时器id必须大于0。Javier Maura向我指出了这个错误(尽管计时器即使id为0也能工作!)。用户也会被警告,如果
SetTimer
失败,则不会报告进度。
- 修正了设置计时器时的一个错误:请求的计时器id是0,而MSDN帮助指出计时器id必须大于0。Javier Maura向我指出了这个错误(尽管计时器即使id为0也能工作!)。用户也会被警告,如果
- 1999-4-7 ATL (v1.1, 内部发布)
-
- 更新了源代码文档,以符合Autoduck 2.0标准。
- 根据Zhuang Yuyao的建议,修正了
CFileInfoArray::AddDir
中的一个错误:如果bRecurse
为false,则bIncludeDirs
未使用。
- 更新了源代码文档,以符合Autoduck 2.0标准。
资源回收
对于FCompare,我借鉴了CDirDialog
,这个目录浏览的不那么常见的对话框包装器,最初由Girish Bharadwaj和Lars Klose开发,后来由Vladimir Kvash增强。顺便说一句,我稍微修改了DirDialog.h
,以便MS VC++在SelChanged
声明中不抱怨未使用csStatusText
和lpcsSelection
。- Giancarlo Iovino开发的Fancy
CHyperLinkEx
,我也对其CHyperLink::OnMouseMove
中的未使用参数nFlag
进行了注释。