方便处理文件名的类






4.85/5 (28投票s)
处理文件名。
引言
作为程序员,我们经常会遇到需要处理文件名并对其执行各种操作的情况。几年前,我发现自己反复编写接收文件名并需要执行一些简单转换(例如更改扩展名)的代码。在大约第十五个包含以下代码的函数之后
TCHAR tszDrive[_MAX_DRIVE],
tszPath[_MAX_PATH],
tszFilename[_MAX_FNAME],
tszExtension[_MAX_EXT];
CString csNewPath;
_tsplitpath(tszFile, tszDrive, tszPath, tszFilename, tszExtension);
.
.
.
csNewPath = tszDrive;
csNewPath += tszPath + tszFilename;
csNewPath += someotherextension;
我意识到大部分工作可以封装在一个类中。背景
您可能认出这个类。它最初发布在 CodeGuru[^] 上,但自那时(1998年8月)以来它已得到扩展,并修复了一些错误。该类现在支持 Unicode。
该类封装了文件名的基本概念,同时兼顾了 UNC 格式的文件名。它还可以将本地机器上使用驱动器字母指定的文件名转换为 UNC 格式。它如何做到这一点取决于文件名的驱动器部分是指本地驱动器还是映射的网络驱动器(我将在本文后面讨论)。
文件名至少有两种形式。第一种形式是(例如)
c:\seti\01\seti.exe
您可以将其分解为四个部分。c:
是硬盘名称。\seti\01\
是通向文件的目录路径。seti
是文件名,.exe
是文件扩展名。
CFileSpec
理解这种格式。该类理解的另一种形式是 UNC 文件名,其形式为
\\Rob\C\seti\01\seti.exe
其中\\Rob\C
是服务器名称和共享名称。\seti\01\
是通向文件的目录路径。seti
是文件名,.exe
是文件扩展名。
类接口
类定义如下所示。class CFileSpec
{
public:
enum FS_BUILTINS
{
FS_EMPTY, // Nothing
FS_APP, // Full application path and name
FS_APPDIR, // Application folder
FS_WINDIR, // Windows folder
FS_SYSDIR, // System folder
FS_TMPDIR, // Temporary folder
FS_DESKTOP, // Desktop folder
FS_FAVOURITES, // Favourites folder
FS_MEDIA, // Default media folder
FS_CURRDIR, // Current folder
FS_TEMPNAME // Create a temporary name
};
CFileSpec(FS_BUILTINS eSpec = FS_EMPTY);
CFileSpec(FS_BUILTINS eSpec, LPCTSTR szFileame);
CFileSpec(LPCTSTR szSpec, LPCTSTR szFilename);
CFileSpec(LPCTSTR szFilename);
// Operations
BOOL Exists() const;
BOOL IsUNCPath() const;
BOOL LoadArchive(CObject *pObj) const;
BOOL SaveArchive(CObject *pObj) const;
// Access functions
CString& Drive() { return m_csDrive; }
CString& Path() { return m_csPath; }
CString& FileName() { return m_csFilename; }
CString& Extension() { return m_csExtension; }
const CString FullPathNoExtension() const;
const CString GetFolder() const;
const CString GetFullSpec() const;
const CString GetFileName() const;
const CString ConvertToUNCPath() const;
void SetFullSpec(LPCTSTR szSpec);
void SetFullSpec(FS_BUILTINS eSpec = FS_EMPTY);
void SetFileName(LPCTSTR szSpec);
void Initialise(FS_BUILTINS eSpec);
private:
BOOL IsUNCPath(LPCTSTR szPath) const;
void WriteAble() const;
void ReadOnly() const;
void GetShellFolder(int iFolder);
CString m_csDrive,
m_csPath,
m_csFilename,
m_csExtension;
};
让我们暂时忽略 FS_BUILTINS
枚举(以及所有使用该枚举的构造函数),看看接受一个或两个字符串的构造函数。最简单的构造函数接受一个字符串,该字符串是文件名和路径。构造函数通过调用 SetFullSpec
将字符串分解为其组成部分,SetFullSpec
又会检查字符串是否为 UNC 格式的文件名。如果不是,它使用 _tsplitpath
来填充 CStrings
。如果它是 UNC 文件名,它会将字符串的服务器/共享名部分(如果存在)复制到 m_csDrive
成员,然后使用 _tsplitpath
分解文件名的其余部分。
然后,您可以使用成员函数检索驱动器、路径、文件名、扩展名或整个路径。您还可以更改这些组成部分中的任何一个,并期望该类在更改后执行合理的处理。例如
CFileSpec fs(_T("c:\seti\01\seti.exe"));
fs.Extension() = _T("dat");
printf(_T("%s\n"), fs.GetFullSpec());
将打印c:\seti\01\seti.dat
表明扩展名已更改。请注意,我之前说过“(如果存在)”。该类完全能够处理不从文件命名空间的顶部开始的文件名,并且它不关心是否是 UNC 路径。(尽管如果它是一个 UNC 路径,根据定义它将以文件命名空间的顶部为根。)
所以现在我们已经将文件名解析为其组成部分。没什么大不了的。该类的强大之处在于一旦你获得了该类的实例,你就可以做些什么。
假设您的应用程序有一个与可执行文件存储在同一目录中的初始化文件?如何获取它?您可以使用 GetModuleFileName()
获取您的可执行文件的名称,然后对返回的值进行一些处理以隔离返回文件名的路径部分,然后将初始化文件名附加到结果中以获取最终文件。它可能看起来有点像这样。
TCHAR tszDrive[_MAX_DRIVE],
tszPath[_MAX_PATH],
tszFile[_MAX_PATH];
CString csNewPath;
GetModuleFileName(NULL, tszFile, _MAX_PATH);
_tsplitpath(tszFile, tszDrive, tszPath, NULL, NULL);
csNewPath = tszDrive + tszPath;
csNewPath += _T("InitialisationFile.ext");
或者你可以这样做。
CFileSpec fs(CFileSpec::FS_APPDIR);
fs.SetFileNameEx(_T("InitialisationFile.ext");
LoadMyInitFile(fs);
甚至这个CFileSpec fs(CFileSpec::FS_APPDIR, _T("InitialisationFile.ext"));
LoadMyInitFile(fs);
执行的是相同的工作,但您需要编写更少的代码。这引出了对我们忽略的 FS_BUILTINS
的讨论。这些,通过构造函数或 SetFullSpec()
重载,允许我们使用我在编写类时认为常用的路径类型初始化一个 CFileSpec
对象。如果您认为我遗漏了什么,很容易扩展列表。(这是一个提示:如果您想添加什么,只需添加即可,不要在这篇文章的留言板上写评论要求我添加,自己写)。尽管如此,我很乐意知道 FS_BUILTINS
中添加了什么,并且肯定会考虑将其添加到下载中。
如果您仔细研究五年前在 CodeGuru 上发布的源代码,您会发现我使用了 Windows 注册表来获取用户桌面和收藏夹文件夹的路径。事实证明,根据 Raymond Chen[^] 的说法,这很糟糕,所以我将该类更改为使用推荐的方法 SHGetSpecialFolderLocation
。(我怀疑如果我继续使用注册表,Mike Dunn 也会对我大加抨击 :))
到目前为止,这是一个非常无趣但可能很有用的类。它变得有趣的地方在于两个特性。
UNC 转换
伊莱恩正在使用一个通过驱动器F:
引用数据的应用程序。她不知道(也不关心)她的机器没有物理驱动器 F:
。不知何故,通过驱动器映射的魔力,服务器上名为 \\Roger\CPData
的文件夹被映射到她机器上的驱动器 F:
。一切都按预期工作,除了网络吞吐量问题,世界一片光明。当伊莱恩关闭她的应用程序时,它会保存对其所用驱动器的引用。稍后,伊恩登录到同一台机器。他打开同一个应用程序,它使用保存的路径尝试访问伊莱恩正在使用的相同文件。如果伊恩和伊莱恩拥有相同的映射驱动器,一切都会正常工作。但如果伊莱恩将驱动器 F:
映射到 \\Roger\CPData
而伊恩将驱动器 F:
映射到 \\Stan\Rants
,那就不妙了。这就是 UNC 转换发挥作用的地方。如果伊莱恩的应用程序将其对 F:/CPData
的引用转换为 UNC 路径,那么当伊恩在他的 ID 下运行相同的应用程序时,它将引用相同的位置,而无需关心伊莱恩和伊恩是否具有相同的驱动器映射。
您调用成员函数 ConvertToUNCPath()
来执行转换。该函数如下所示。
const CString CFileSpec::ConvertToUNCPath() const
{
USES_CONVERSION;
CString csPath = GetFullSpec();
if (IsUNCPath(csPath))
return csPath;
if (csPath[1] == ':')
{
// Fully qualified pathname including a drive letter, check if it's a
// mapped drive
UINT uiDriveType = GetDriveType(m_csDrive);
if (uiDriveType & DRIVE_REMOTE)
{
// Yup - it's mapped so convert to a UNC path...
TCHAR tszTemp[_MAX_PATH];
UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp;
DWORD dwSize = _MAX_PATH;
DWORD dwRet = WNetGetUniversalName(m_csDrive,
REMOTE_NAME_INFO_LEVEL, uncName,
&dwSize);
CString csDBShare;
if (dwRet == NO_ERROR)
return uncName->lpUniversalName + m_csPath + m_csFilename
+ m_csExtension;
}
else
{
// It's a local drive so search for a share to it...
NET_API_STATUS res;
PSHARE_INFO_502 BufPtr,
p;
DWORD er = 0,
tr = 0,
resume = 0,
i;
int iBestMatch = 0;
CString csTemp,
csTempDrive,
csBestMatch;
do
{
res = NetShareEnum(NULL, 502, (LPBYTE *) &BufPtr, DWORD(-1), &er, &tr,
&resume);
//
// If the call succeeds,
//
if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
{
csTempDrive = GetFolder();
csTempDrive.MakeLower();
p = BufPtr;
//
// Loop through the entries;
//
for (i = 1; i <= er; i++)
{
if (p->shi502_type == STYPE_DISKTREE)
{
csTemp = W2A((LPWSTR) p->shi502_path);
csTemp.MakeLower();
if (csTempDrive.Find(csTemp) == 0)
{
// We found a match
if (iBestMatch < csTemp.GetLength())
{
iBestMatch = csTemp.GetLength();
csBestMatch = W2A((LPWSTR) p->shi502_netname);
}
}
}
p++;
}
//
// Free the allocated buffer.
//
NetApiBufferFree(BufPtr);
if (iBestMatch)
{
TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dwBufLen = countof(tszComputerName);
csTemp = GetFolder();
csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch + 1);
GetComputerName(tszComputerName, &dwBufLen);
csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName,
csBestMatch, csTemp,
m_csFilename, m_csExtension);
}
}
else
TRACE(_T("Error: %ld\n"), res);
// Continue to call NetShareEnum while
// there are more entries.
//
} while (res == ERROR_MORE_DATA); // end do
}
}
return csPath;
}
该函数是 const
,这意味着它不会修改底层的 CFileSpec
对象。它只是将该对象表示的路径转换为 UNC 路径(如果可能)。无论哪种方式,它都返回一个 CString
。该函数首先检查对象是否已包含 UNC 路径。如果是,它返回该路径。
如果不是,该函数接着检查对象是否包含根植于文件系统命名空间的路径。也就是说,它是否包含驱动器值。这是通过以下行完成的
if (csPath[1] == ':')
如果此测试失败,我们只需返回对象所代表的路径(无法转换)。假设测试通过,我们有两种可能性。路径可能在共享上,也可能在本地存储上。更简单的情况是路径在另一台机器的共享上(即在映射驱动器上)。我们使用 GetDriveType()
API 获取驱动器类型,并分支到两条路径之一。如果它是远程驱动器(路径在共享上),我们执行此代码。
// Yup - it's mapped so convert to a UNC path...
TCHAR tszTemp[_MAX_PATH];
UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp;
DWORD dwSize = _MAX_PATH;
DWORD dwRet = WNetGetUniversalName(m_csDrive, REMOTE_NAME_INFO_LEVEL,
uncName, &dwSize);
CString csDBShare;
if (dwRet == NO_ERROR)
return uncName->lpUniversalName + m_csPath + m_csFilename + m_csExtension;
这非常简单。我们调用 WNetGetUniversalName()
API,传入驱动器名称(c:、d: 等),如果 API 成功,我们将获得一个服务器/共享名称,然后我们将路径的其余部分附加到该名称上。如果 API 失败(无论出于何种原因),UNC 转换方法将返回未转换的路径。有趣之处在于当路径指向本地机器上的驱动器时。在这种情况下,我们希望在此机器上搜索可以从另一台机器访问的共享。
为此,我们需要枚举共享。当然,事情没那么简单。共享可以是打印队列、磁盘驱动器或 IPC 通道。我们的代码需要忽略除驱动器共享之外的任何共享。我们通过检查枚举的每个共享的共享类型来做到这一点。这很简单。我们使用 NetShareEnum
API 设置枚举,然后进入一个循环,检查返回的每个共享。对于每个磁盘共享,我们检查与该共享关联的路径 p->shi502_path
。
现在事情变得复杂了。您的机器上可能有一个以上可以到达该路径的共享。例如,您可能有一个针对 c:
的共享,以及另一个针对 c:\seti
的共享。在不考虑权限的情况下(此代码不考虑权限),不一定有最佳路径。我编写的代码寻找“最佳匹配”,我将“最佳匹配”定义为最长的服务器/共享名。换句话说,如果给定 c:
和 c:\seti
之间的选择,并且我正在寻找与 c:\seti\somefile
的匹配,代码将选择最长的匹配。当然,在选择了最长的匹配后,它必须删除路径的某些部分。
这是在此代码中完成的
if (iBestMatch)
{
TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dwBufLen = countof(tszComputerName);
csTemp = GetFolder();
csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch);
GetComputerName(tszComputerName, &dwBufLen);
csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName, csBestMatch, csTemp,
m_csFilename, m_csExtension);
}
我们删除路径中包含在共享中的那部分。例如,如果机器 Rob
上的共享名为 seti
并且表示该机器上的 c:\seti
,并且路径是 c:\seti\setisrv.exe
,则返回的 UNC 路径将是 \\Rob\seti\setisrv.exe
。一个陷阱
如果你仔细检查了代码,你可能会被这行代码搞糊涂。csTemp = W2A((LPWSTR) p->shi502_path);
您会注意到该函数使用了 USES_CONVERSION
宏。这个宏设置了一些局部变量,以便在函数内部进行 Unicode 到 MBCS/ANSI 的转换(反之亦然)。如果您了解 USES_CONVERSION
,那么您可能也了解 W2A
宏。这个宏执行从宽字符到 MBCS/ANSI 的转换,即从 Unicode 到 MBCS/ANSI。如果在一个 ANSI/MBCS 应用程序中编译,该宏期望看到一个 Unicode 输入字符串 (LPWSTR)。嗯,所以包含的示例项目不是 Unicode。那么为什么还要强制转换为 LPWSTR
呢?事实证明,在我这台机器上的 PSDK 版本中,如果它是 ANSI/MBCS 构建,SHARE_INFO_502
结构将其成员变量定义为 LPSTR
;如果它是 Unicode 构建,则定义为 LPWSTR
。听起来不错,但有一点除外。无论构建如何完成,操作系统都会返回 Unicode 字符串!(Win2k SP4)。如果您进行了 MBCS/ANSI 构建,除非您进行类型转换并使用 W2A
宏,否则代码将返回错误的路径。所以即使编译器认为在 ANSI/MBCS 构建中这些成员是 ANSI/MBCS,它们实际上是 Unicode。但是如果您传递非 LPWSTR
参数给 W2A
宏,它会报错。因此需要进行类型转换。这让我挠头了几分钟。序列化支持
许多人不喜欢 MFC 的序列化支持。我还没有看到令人信服的反驳理由,所以这个类包含了对它的支持。这个支持不是原始类的一部分。我只是注意到我写了很多相似的代码,所以我添加了它,并且不再需要为我编写的每个新应用程序编写几乎相同的代码。open a file create an archive object attach the file to the archive serialise the object through the archive detach the file from the archive close the archive close the file在我看来,向这个类添加 MFC 存档支持是理所当然的——事实也确实如此。您已经在类头文件中看到了上面定义的函数。这是代码。
BOOL CFilename::LoadArchive(CObject *pObj) const
{
CFile file;
BOOL bStatus = FALSE;
ASSERT(pObj);
ASSERT_VALID(pObj);
ASSERT_KINDOF(CObject, pObj);
ASSERT(pObj->IsSerializable());
if (Exists())
{
try
{
if (file.Open(GetFullSpec(), CFile::modeRead | CFile::typeBinary
| CFile::shareExclusive))
{
CArchive ar(&file, CArchive::load);
pObj->Serialize(ar);
ar.Close();
file.Close();
bStatus = TRUE;
}
}
catch(CException *e)
{
e->Delete();
}
}
return bStatus;
}
代码很简单。检查文件是否存在。如果存在,打开它,将存档对象附加到文件对象,然后将其序列化到传递的对象指针中。当然,指向的对象必须支持序列化,因此有调试检查。保存存档的代码几乎完全相同。演示应用程序
演示应用程序执行了一些简单的路径到翻译路径的转换。它还显示了底层CFileSpec
对象的成员变量。这里显示的示例演示了该类如何将引用转换为我妻子的机器(f:\seti\setispy.ini),其中 f: 映射到 \\Suzy\C。鸣谢
示例项目使用了一个由 P J Arends 编写的类CFileEditCtrl
。一个非常棒的类,可以在这里找到。历史
2003年12月24日,CodeProject 初始版本。
2003年12月24日,修复了PJ Arends指出的一个bug。
2003年12月27日,修复了Mike Dunn指出的一个bug。