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

方便处理文件名的类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (28投票s)

2003年12月24日

CPOL

10分钟阅读

viewsIcon

156697

downloadIcon

2718

处理文件名。

引言

作为程序员,我们经常会遇到需要处理文件名并对其执行各种操作的情况。几年前,我发现自己反复编写接收文件名并需要执行一些简单转换(例如更改扩展名)的代码。在大约第十五个包含以下代码的函数之后

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。

The demo application

第二个示例演示了该类如何转换 UNC 输入路径。

The demo application

鸣谢

示例项目使用了一个由 P J Arends 编写的类 CFileEditCtrl。一个非常棒的类,可以在这里找到。

历史

2003年12月24日,CodeProject 初始版本。

2003年12月24日,修复了PJ Arends指出的一个bug。

2003年12月27日,修复了Mike Dunn指出的一个bug。

© . All rights reserved.