如何从绝对路径获得相对路径


174

我的应用程序中有一部分显示用户通过OpenFileDialog加载的文件路径。它占用太多空间来显示整个路径,但是我不想只显示文件名,因为它可能会含糊不清。因此,我希望显示相对于assembly / exe目录的文件路径。

例如,程序集驻留在C:\Program Files\Dummy Folder\MyProgram,文件驻留在,C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat然后我希望它显示.\Data\datafile1.dat。如果文件在里面C:\Program Files\Dummy Folder\datafile1.dat,那我会想要的..\datafile1.dat。但是,如果文件位于根目录或根目录下的1个目录,则显示完整路径。

您会推荐什么解决方案?正则表达式?

基本上,我想显示有用的文件路径信息,而不占用太多屏幕空间。

编辑:只是要澄清一点。该解决方案的目的是帮助用户或我自己知道我上次加载哪个文件以及大致从哪个目录加载。我正在使用只读文本框来显示路径。在大多数情况下,文件路径比文本框的显示空间长得多。该路径应该是有用的,但不足以占用更多屏幕空间。

Alex Brault的评论很好,Jonathan Leffler也很好。DavidK提供的Win32函数只能解决部分问题,而不能解决全部问题,但还是要感谢。至于James Newton-King解决方案,有空时我会再尝试。


Answers:


192

.NET Core 2.0可以Path.GetRelativePath使用它。

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path or <c>toPath</c> if the paths are not related.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static String MakeRelativePath(String fromPath, String toPath)
{
    if (String.IsNullOrEmpty(fromPath)) throw new ArgumentNullException("fromPath");
    if (String.IsNullOrEmpty(toPath))   throw new ArgumentNullException("toPath");

    Uri fromUri = new Uri(fromPath);
    Uri toUri = new Uri(toPath);

    if (fromUri.Scheme != toUri.Scheme) { return toPath; } // path can't be made relative.

    Uri relativeUri = fromUri.MakeRelativeUri(toUri);
    String relativePath = Uri.UnescapeDataString(relativeUri.ToString());

    if (toUri.Scheme.Equals("file", StringComparison.InvariantCultureIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}

32
经过大量测试后,此方法最适合我。您需要记住,Uri将不以路径分隔符结尾的文件夹视为文件(如果bar是文件夹,请使用c:\​​ foo \ bar \而不是c:\ foo \ bar)。
VVS

6
解决斜线问题的一般方法是使用return relativeUri.ToString().Replace('/',Path.DirectorySeparatorChar);
Nyerguds 2011年

4
您应该取消转义由此创建的相对uri以获得有效路径。.ToString()表示形式将包含无效且在路径中不必要的转义序列。
Eamon Nerbonne 2011年

3
在arg检查(fromPath.Last()!= Path.DirectorySeparatorChar){fromPath + = Path.DirectorySeparatorChar;之后添加以下内容 } if(toPath.Last()!= Path.DirectorySeparatorChar){toPath + = Path.DirectorySeparatorChar; }
西蒙(Simon)

8
对我来说,这不是返回相对路径。对于它c:\testc:\test\abc.txt它返回的test\abc.txt结果与我的看法无关。我希望abc.txt
Juergen d 2015年

51

这个问题有点晚了,但是我也需要这个功能。我同意DavidK的观点,因为有一个内置的API函数可以提供此功能,所以您应该使用它。这是一个托管包装器:

public static string GetRelativePath(string fromPath, string toPath)
{
    int fromAttr = GetPathAttribute(fromPath);
    int toAttr = GetPathAttribute(toPath);

    StringBuilder path = new StringBuilder(260); // MAX_PATH
    if(PathRelativePathTo(
        path,
        fromPath,
        fromAttr,
        toPath,
        toAttr) == 0)
    {
        throw new ArgumentException("Paths must have a common prefix");
    }
    return path.ToString();
}

private static int GetPathAttribute(string path)
{
    DirectoryInfo di = new DirectoryInfo(path);
    if (di.Exists)
    {
        return FILE_ATTRIBUTE_DIRECTORY;
    }

    FileInfo fi = new FileInfo(path);
    if(fi.Exists)
    {
        return FILE_ATTRIBUTE_NORMAL;
    }

    throw new FileNotFoundException();
}

private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
private const int FILE_ATTRIBUTE_NORMAL = 0x80;

[DllImport("shlwapi.dll", SetLastError = true)]
private static extern int PathRelativePathTo(StringBuilder pszPath, 
    string pszFrom, int dwAttrFrom, string pszTo, int dwAttrTo);

4
如果文件或路径不存在,我不会抛出异常,因为这可能是完全合法的情况。
VVS

2
那么,GetPathAttributes将返回什么?没有“文件不存在”的标志,因此除了抛出以外,我看不到任何可行的选择,否则调用者会得到错误的信息。
ctacke

2
请注意,如果无法创建相对路径,则PathRelativePathTo返回FALSE。在这种情况下,您应该返回String.Empty或引发异常。
丹尼尔·罗斯

4
我发现它更清晰:它允许诸如bool success = PathRelativePathTo(...)之类的代码比int易于理解,在int中,您需要阅读有关int含义的文档。
丹尼尔·罗斯

4
伙计们……您可以删除整个GetPathAttribute。只要您完全确定给出的参数是目录,您只需将其设置为0x10即可使用完全不存在的路径。就我而言,首选解决方案是简单地返回完整的绝对目标路径,而不是抛出该异常。
Nyerguds '16

31

.NET Core 2.0答案

.NET Core 2.0具有Path.GetRelativePath,可以像这样使用:

var relativePath = Path.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

在上面的示例中,relativePath变量等于Data\datafile1.dat

替代.NET答案

当文件路径不以正斜杠字符(/)结尾时,@ Dave的解决方案不起作用,如果该路径是目录路径,则可能会发生这种情况。我的解决方案解决了该问题,并且还使用了Uri.UriSchemeFile常量而不是硬编码"FILE"

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fromPath"/> or <paramref name="toPath"/> is <c>null</c>.</exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static string GetRelativePath(string fromPath, string toPath)
{
    if (string.IsNullOrEmpty(fromPath))
    {
        throw new ArgumentNullException("fromPath");
    }

    if (string.IsNullOrEmpty(toPath))
    {
        throw new ArgumentNullException("toPath");
    }

    Uri fromUri = new Uri(AppendDirectorySeparatorChar(fromPath));
    Uri toUri = new Uri(AppendDirectorySeparatorChar(toPath));

    if (fromUri.Scheme != toUri.Scheme)
    {
        return toPath;
    }

    Uri relativeUri = fromUri.MakeRelativeUri(toUri);
    string relativePath = Uri.UnescapeDataString(relativeUri.ToString());

    if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}

private static string AppendDirectorySeparatorChar(string path)
{
    // Append a slash only if the path is a directory and does not have a slash.
    if (!Path.HasExtension(path) &&
        !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        return path + Path.DirectorySeparatorChar;
    }

    return path;
}

Windows Interop答案

有一个Windows API,称为PathRelativePathToA,可用于查找相对路径。请注意,传递给该函数的文件或目录路径必须存在才能使其正常工作。

var relativePath = PathExtended.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

public static class PathExtended
{
    private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
    private const int FILE_ATTRIBUTE_NORMAL = 0x80;
    private const int MaximumPath = 260;

    public static string GetRelativePath(string fromPath, string toPath)
    {
        var fromAttribute = GetPathAttribute(fromPath);
        var toAttribute = GetPathAttribute(toPath);

        var stringBuilder = new StringBuilder(MaximumPath);
        if (PathRelativePathTo(
            stringBuilder,
            fromPath,
            fromAttribute,
            toPath,
            toAttribute) == 0)
        {
            throw new ArgumentException("Paths must have a common prefix.");
        }

        return stringBuilder.ToString();
    }

    private static int GetPathAttribute(string path)
    {
        var directory = new DirectoryInfo(path);
        if (directory.Exists)
        {
            return FILE_ATTRIBUTE_DIRECTORY;
        }

        var file = new FileInfo(path);
        if (file.Exists)
        {
            return FILE_ATTRIBUTE_NORMAL;
        }

        throw new FileNotFoundException(
            "A file or directory with the specified path was not found.",
            path);
    }

    [DllImport("shlwapi.dll", SetLastError = true)]
    private static extern int PathRelativePathTo(
        StringBuilder pszPath,
        string pszFrom,
        int dwAttrFrom,
        string pszTo,
        int dwAttrTo);
}

1
效果更像人期望的那样-我建议使用此方法,而不是最高评价的答案。
–theMayer

您所认为AltDirectorySeparatorChar的可能性是,也不应AppendDirectorySeparatorChar检查吗?
Ohad Schneider

另外,文件可能没有扩展名,因此尽管在大多数情况下这可能更方便,但不允许您指定这种情况。也许要检查文件系统条目是否存在,如果存在,请检查其是文件还是文件夹。如果不存在,请遵循此逻辑。或者甚至可以在签名中添加某种方式来指定是否提供了文件或目录(例如2个布尔值)。
Ohad Schneider

最后,如果方案不同,我将抛出一个例外。
Ohad Schneider

25

shlwapi.dll中有一个Win32(C ++)函数,可以完全满足您的要求: PathRelativePathTo()

但是,除了P / Invoke之外,我不知道从.NET访问此方法的任何方法。


3
它对哪一部分没有帮助?阅读原始帖子后,PathRelativePathTo()对我来说看起来像您想要的那样,但这可能是因为我误解了某些东西……
DavidK

3
完美地工作。有关如何设置P / Invoke的信息,请参见pinvoke.net/default.aspx/shlwapi.PathRelativePathTo
joce 2011年

2
谢谢!我实际上是在寻找C ++解决方案!
NTDLS 2011年

2
值得一提的是功能shlwapi.dll是现在已经过时msdn.microsoft.com/en-us/library/windows/desktop/... "These functions are available through Windows XP Service Pack 2 (SP2) and Windows Server 2003. They might be altered or unavailable in subsequent versions of Windows."
基本

4
我没有读过该页面,因为它不建议使用shlwapi.dll本身:它说的是不建议使用该页面上列出的shlwapi.dll的包装函数。该页面上未提及PathRelativePathTo()本身,并且PathRelativePathTo()的主要文档未提及弃用,因此据我所知,它仍然是有效的调用函数。
DavidK 2014年

14

如果您使用的是.NET Core 2.0,Path.GetRelativePath()则可提供以下特定功能:

        var relativeTo = @"C:\Program Files\Dummy Folder\MyProgram";
        var path = @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat";

        string relativePath = System.IO.Path.GetRelativePath(relativeTo, path);

        System.Console.WriteLine(relativePath);
        // output --> Data\datafile1.dat 

否则,对于.NET完整框架(自v4.7起),建议使用其他建议的答案之一。


9

我过去曾经使用过。

/// <summary>
/// Creates a relative path from one file
/// or folder to another.
/// </summary>
/// <param name="fromDirectory">
/// Contains the directory that defines the
/// start of the relative path.
/// </param>
/// <param name="toPath">
/// Contains the path that defines the
/// endpoint of the relative path.
/// </param>
/// <returns>
/// The relative path from the start
/// directory to the end path.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public static string MakeRelative(string fromDirectory, string toPath)
{
  if (fromDirectory == null)
    throw new ArgumentNullException("fromDirectory");

  if (toPath == null)
    throw new ArgumentNullException("toPath");

  bool isRooted = (Path.IsPathRooted(fromDirectory) && Path.IsPathRooted(toPath));

  if (isRooted)
  {
    bool isDifferentRoot = (string.Compare(Path.GetPathRoot(fromDirectory), Path.GetPathRoot(toPath), true) != 0);

    if (isDifferentRoot)
      return toPath;
  }

  List<string> relativePath = new List<string>();
  string[] fromDirectories = fromDirectory.Split(Path.DirectorySeparatorChar);

  string[] toDirectories = toPath.Split(Path.DirectorySeparatorChar);

  int length = Math.Min(fromDirectories.Length, toDirectories.Length);

  int lastCommonRoot = -1;

  // find common root
  for (int x = 0; x < length; x++)
  {
    if (string.Compare(fromDirectories[x], toDirectories[x], true) != 0)
      break;

    lastCommonRoot = x;
  }

  if (lastCommonRoot == -1)
    return toPath;

  // add relative folders in from path
  for (int x = lastCommonRoot + 1; x < fromDirectories.Length; x++)
  {
    if (fromDirectories[x].Length > 0)
      relativePath.Add("..");
  }

  // add to folders to path
  for (int x = lastCommonRoot + 1; x < toDirectories.Length; x++)
  {
    relativePath.Add(toDirectories[x]);
  }

  // create relative path
  string[] relativeParts = new string[relativePath.Count];
  relativePath.CopyTo(relativeParts, 0);

  string newPath = string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts);

  return newPath;
}

1
我会研究它,我需要一些时间对其进行测试。谢谢
错误的

1
我建议使用Path.GetFullPath()成功比较两条具有相对位的路径。示例:c:\ a \ .. \ b vs. c:\ b vs.c:\ b \。\
VVS

5

正如Alex Brault指出的,尤其是在Windows上,绝对路径(带有驱动器号和所有字母)是明确的,并且通常更好。

您的OpenFileDialog不应该使用常规的树状浏览器结构吗?

为了获得一些命名法,RefDir是您要指定相对路径的目录。该AbsName是要映射的绝对路径名; 和RelPath是生成的相对路径。

请选择以下匹配的第一个选项:

  • 如果驱动器号不同,则从RefDir到AbsName没有相对路径;您必须使用AbsName。
  • 如果AbsName位于RefDir的子目录中,或者是RefDir中的文件,则只需从AbsName的开头删除RefDir即可创建RelPath。可以选择在“ ./”(或“。\”)之前加上前缀(因为您使用的是Windows)。
  • 查找RefDir和AbsName的最长公共前缀(其中D:\ Abc \ Def和D:\ Abc \ Default共享D:\ Abc作为最长公共前缀;它必须是名称组件的映射,而不是简单的最长公共前缀子串); 称为LCP。从AbsName和RefDir删除LCP。对于(RefDir-LCP)中剩余的每个路径分量,在(AbsName-LCP)之前添加“ .. \”以生成RelPath。

为了说明最后一条规则(当然,这是迄今为止最复杂的规则),请开始:

RefDir = D:\Abc\Def\Ghi
AbsName = D:\Abc\Default\Karma\Crucible

然后

LCP = D:\Abc
(RefDir - LCP) = Def\Ghi
(Absname - LCP) = Default\Karma\Crucible
RelPath = ..\..\Default\Karma\Crucible

当我打字时,DavidK给出了一个答案,表明您不是第一个需要此功能的人,并且有一个标准函数可以完成此工作。 用它。 但是,无论是从第一条原则出发还是以自己的方式思考,也没有什么害处。

除了Unix系统不支持驱动器号(因此所有内容始终位于同一根目录下,因此第一个项目符号无关)之外,在Unix上可以使用相同的技术。


4

它有很长的路要走,但是System.Uri类有一个名为MakeRelativeUri的方法。也许您可以使用它。真遗憾的是System.IO.Path没有此功能。


4

如上所述 .NET Core 2.x具有的实现Path.GetRelativePath

以下代码是从源代码改编而成的,并且可以与.NET 4.7.1 Framework正常运行。

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

//Adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Path.cs#L697
// by Anton Krouglov

using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Text;
using Xunit;

namespace System.IO {
    // Provides methods for processing file system strings in a cross-platform manner.
    // Most of the methods don't do a complete parsing (such as examining a UNC hostname), 
    // but they will handle most string operations.
    public static class PathNetCore {

        /// <summary>
        /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
        /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
        /// </summary>
        /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
        /// <param name="path">The destination path.</param>
        /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
        public static string GetRelativePath(string relativeTo, string path) {
            return GetRelativePath(relativeTo, path, StringComparison);
        }

        private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) {
            if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
            Debug.Assert(comparisonType == StringComparison.Ordinal ||
                         comparisonType == StringComparison.OrdinalIgnoreCase);

            relativeTo = Path.GetFullPath(relativeTo);
            path = Path.GetFullPath(path);

            // Need to check if the roots are different- if they are we need to return the "to" path.
            if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType))
                return path;

            int commonLength = PathInternalNetCore.GetCommonPathLength(relativeTo, path,
                ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);

            // If there is nothing in common they can't share the same root, return the "to" path as is.
            if (commonLength == 0)
                return path;

            // Trailing separators aren't significant for comparison
            int relativeToLength = relativeTo.Length;
            if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo))
                relativeToLength--;

            bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path);
            int pathLength = path.Length;
            if (pathEndsInSeparator)
                pathLength--;

            // If we have effectively the same path, return "."
            if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";

            // We have the same root, we need to calculate the difference now using the
            // common Length and Segment count past the length.
            //
            // Some examples:
            //
            //  C:\Foo C:\Bar L3, S1 -> ..\Bar
            //  C:\Foo C:\Foo\Bar L6, S0 -> Bar
            //  C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
            //  C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar

            StringBuilder
                sb = new StringBuilder(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));

            // Add parent segments for segments past the common on the "from" path
            if (commonLength < relativeToLength) {
                sb.Append("..");

                for (int i = commonLength + 1; i < relativeToLength; i++) {
                    if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) {
                        sb.Append(DirectorySeparatorChar);
                        sb.Append("..");
                    }
                }
            }
            else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) {
                // No parent segments and we need to eat the initial separator
                //  (C:\Foo C:\Foo\Bar case)
                commonLength++;
            }

            // Now add the rest of the "to" path, adding back the trailing separator
            int differenceLength = pathLength - commonLength;
            if (pathEndsInSeparator)
                differenceLength++;

            if (differenceLength > 0) {
                if (sb.Length > 0) {
                    sb.Append(DirectorySeparatorChar);
                }

                sb.Append(path, commonLength, differenceLength);
            }

            return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb);
        }

        // Public static readonly variant of the separators. The Path implementation itself is using
        // internal const variant of the separators for better performance.
        public static readonly char DirectorySeparatorChar = PathInternalNetCore.DirectorySeparatorChar;
        public static readonly char AltDirectorySeparatorChar = PathInternalNetCore.AltDirectorySeparatorChar;
        public static readonly char VolumeSeparatorChar = PathInternalNetCore.VolumeSeparatorChar;
        public static readonly char PathSeparator = PathInternalNetCore.PathSeparator;

        /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
        internal static StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
    }

    /// <summary>Contains internal path helpers that are shared between many projects.</summary>
    internal static class PathInternalNetCore {
        internal const char DirectorySeparatorChar = '\\';
        internal const char AltDirectorySeparatorChar = '/';
        internal const char VolumeSeparatorChar = ':';
        internal const char PathSeparator = ';';

        internal const string ExtendedDevicePathPrefix = @"\\?\";
        internal const string UncPathPrefix = @"\\";
        internal const string UncDevicePrefixToInsert = @"?\UNC\";
        internal const string UncExtendedPathPrefix = @"\\?\UNC\";
        internal const string DevicePathPrefix = @"\\.\";

        //internal const int MaxShortPath = 260;

        // \\?\, \\.\, \??\
        internal const int DevicePrefixLength = 4;

        /// <summary>
        /// Returns true if the two paths have the same root
        /// </summary>
        internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) {
            int firstRootLength = GetRootLength(first);
            int secondRootLength = GetRootLength(second);

            return firstRootLength == secondRootLength
                   && string.Compare(
                       strA: first,
                       indexA: 0,
                       strB: second,
                       indexB: 0,
                       length: firstRootLength,
                       comparisonType: comparisonType) == 0;
        }

        /// <summary>
        /// Gets the length of the root of the path (drive, share, etc.).
        /// </summary>
        internal static int GetRootLength(string path) {
            int i = 0;
            int volumeSeparatorLength = 2; // Length to the colon "C:"
            int uncRootLength = 2; // Length to the start of the server name "\\"

            bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix);
            bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix);
            if (extendedSyntax) {
                // Shift the position we look for the root from to account for the extended prefix
                if (extendedUncSyntax) {
                    // "\\" -> "\\?\UNC\"
                    uncRootLength = UncExtendedPathPrefix.Length;
                }
                else {
                    // "C:" -> "\\?\C:"
                    volumeSeparatorLength += ExtendedDevicePathPrefix.Length;
                }
            }

            if ((!extendedSyntax || extendedUncSyntax) && path.Length > 0 && IsDirectorySeparator(path[0])) {
                // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")

                i = 1; //  Drive rooted (\foo) is one character
                if (extendedUncSyntax || (path.Length > 1 && IsDirectorySeparator(path[1]))) {
                    // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
                    // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
                    i = uncRootLength;
                    int n = 2; // Maximum separators to skip
                    while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
                }
            }
            else if (path.Length >= volumeSeparatorLength &&
                     path[volumeSeparatorLength - 1] == PathNetCore.VolumeSeparatorChar) {
                // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
                // If the colon is followed by a directory separator, move past it
                i = volumeSeparatorLength;
                if (path.Length >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
            }

            return i;
        }

        /// <summary>
        /// True if the given character is a directory separator.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsDirectorySeparator(char c) {
            return c == PathNetCore.DirectorySeparatorChar || c == PathNetCore.AltDirectorySeparatorChar;
        }

        /// <summary>
        /// Get the common path length from the start of the string.
        /// </summary>
        internal static int GetCommonPathLength(string first, string second, bool ignoreCase) {
            int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);

            // If nothing matches
            if (commonChars == 0)
                return commonChars;

            // Or we're a full string and equal length or match to a separator
            if (commonChars == first.Length
                && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
                return commonChars;

            if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
                return commonChars;

            // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
            while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
                commonChars--;

            return commonChars;
        }

        /// <summary>
        /// Gets the count of common characters from the left optionally ignoring case
        /// </summary>
        internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) {
            if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;

            int commonChars = 0;

            fixed (char* f = first)
            fixed (char* s = second) {
                char* l = f;
                char* r = s;
                char* leftEnd = l + first.Length;
                char* rightEnd = r + second.Length;

                while (l != leftEnd && r != rightEnd
                                    && (*l == *r || (ignoreCase &&
                                                     char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) {
                    commonChars++;
                    l++;
                    r++;
                }
            }

            return commonChars;
        }

        /// <summary>
        /// Returns true if the path ends in a directory separator.
        /// </summary>
        internal static bool EndsInDirectorySeparator(string path)
            => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
    }

    /// <summary> Tests for PathNetCore.GetRelativePath </summary>
    public static class GetRelativePathTests {
        [Theory]
        [InlineData(@"C:\", @"C:\", @".")]
        [InlineData(@"C:\a", @"C:\a\", @".")]
        [InlineData(@"C:\A", @"C:\a\", @".")]
        [InlineData(@"C:\a\", @"C:\a", @".")]
        [InlineData(@"C:\", @"C:\b", @"b")]
        [InlineData(@"C:\a", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\b\", @"..\b\")]
        [InlineData(@"C:\a\b", @"C:\a", @"..")]
        [InlineData(@"C:\a\b", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\a\b", @"b")]
        [InlineData(@"C:\a", @"C:\A\b", @"b")]
        [InlineData(@"C:\a", @"C:\b\c", @"..\b\c")]
        [InlineData(@"C:\a\", @"C:\a\b", @"b")]
        [InlineData(@"C:\", @"D:\", @"D:\")]
        [InlineData(@"C:\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\", @"D:\b\", @"D:\b\")]
        [InlineData(@"C:\a", @"D:\b", @"D:\b")]
        [InlineData(@"C:\a\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\ab", @"C:\a", @"..\a")]
        [InlineData(@"C:\a", @"C:\ab", @"..\ab")]
        [InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")]
        [InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")]
        //[PlatformSpecific(TestPlatforms.Windows)]  // Tests Windows-specific paths
        public static void GetRelativePath_Windows(string relativeTo, string path, string expected) {
            string result = PathNetCore.GetRelativePath(relativeTo, path);
            Assert.Equal(expected, result);

            // Check that we get the equivalent path when the result is combined with the sources
            Assert.Equal(
                Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar),
                Path.GetFullPath(Path.Combine(Path.GetFullPath(relativeTo), result))
                    .TrimEnd(Path.DirectorySeparatorChar),
                ignoreCase: true,
                ignoreLineEndingDifferences: false,
                ignoreWhiteSpaceDifferences: false);
        }
    }
}

哇,非常感谢!这与.NET Framework 4.6.1完美配合。这个答案应该被赞成甚至被接受为解决方案。
j00hi

谢谢!稍作修改,它就可以在.NET Framework 4.5.2
Bja

3

我正在使用这个:

public static class StringExtensions
{
  /// <summary>
  /// Creates a relative path from one file or folder to another.
  /// </summary>
  /// <param name="absPath">Absolute path.</param>
  /// <param name="relTo">Directory that defines the start of the relative path.</param> 
  /// <returns>The relative path from the start directory to the end path.</returns>
  public static string MakeRelativePath(this string absPath, string relTo)
  {
      string[] absParts = absPath.Split(Path.DirectorySeparatorChar);
      string[] relParts = relTo.Split(Path.DirectorySeparatorChar);

      // Get the shortest of the two paths
      int len = absParts.Length < relParts.Length
          ? absParts.Length : relParts.Length;

      // Use to determine where in the loop we exited
      int lastCommonRoot = -1;
      int index;

      // Find common root
      for (index = 0; index < len; index++)
      {
          if (absParts[index].Equals(relParts[index], StringComparison.OrdinalIgnoreCase))
              lastCommonRoot = index;
          else 
            break;
      }

      // If we didn't find a common prefix then throw
      if (lastCommonRoot == -1)
          throw new ArgumentException("The path of the two files doesn't have any common base.");

      // Build up the relative path
      var relativePath = new StringBuilder();

      // Add on the ..
      for (index = lastCommonRoot + 1; index < relParts.Length; index++)
      {
        relativePath.Append("..");
        relativePath.Append(Path.DirectorySeparatorChar);
      }

      // Add on the folders
      for (index = lastCommonRoot + 1; index < absParts.Length - 1; index++)
      {
        relativePath.Append(absParts[index]);
        relativePath.Append(Path.DirectorySeparatorChar);
      }
      relativePath.Append(absParts[absParts.Length - 1]);

      return relativePath.ToString();
  }
}

3

用:

RelPath = AbsPath.Replace(ApplicationPath, ".")

在少数情况下,这将非常有用!我要用这个!我认为其他人想要通用的错误检查,边缘情况处理解决方案。但是,如果这是您所需要的,那肯定很简单!
DanO 2013年

我最终使用path.Replace(rootPath.TrimEnd('\\') + "\\", "")
Konard

2

如果您确定绝对路径2总是相对于绝对路径,则只需从path2中删除前N个字符,其中N是path1的长度。


2

您要使用CommonPath此类的方法RelativePath。有了公用路径后,只需将其从要显示的路径中剥离即可。

Namespace IO.Path

    Public NotInheritable Class RelativePath

        Private Declare Function PathRelativePathTo Lib "shlwapi" Alias "PathRelativePathToA" ( _
            ByVal pszPath As String, _
            ByVal pszFrom As String, _
            ByVal dwAttrFrom As Integer, _
            ByVal pszTo As String, _
            ByVal dwAttrTo As Integer) As Integer

        Private Declare Function PathCanonicalize Lib "shlwapi" Alias "PathCanonicalizeA" ( _
            ByVal pszBuf As String, _
            ByVal pszPath As String) As Integer

        Private Const FILE_ATTRIBUTE_DIRECTORY As Short = &H10S

        Private Const MAX_PATH As Short = 260

        Private _path As String
        Private _isDirectory As Boolean

#Region " Constructors "

        Public Sub New()

        End Sub

        Public Sub New(ByVal path As String)
            _path = path
        End Sub

        Public Sub New(ByVal path As String, ByVal isDirectory As Boolean)
            _path = path
            _isDirectory = isDirectory
        End Sub

#End Region

        Private Shared Function StripNulls(ByVal value As String) As String
            StripNulls = value
            If (InStr(value, vbNullChar) > 0) Then
                StripNulls = Left(value, InStr(value, vbNullChar) - 1)
            End If
        End Function

        Private Shared Function TrimCurrentDirectory(ByVal path As String) As String
            TrimCurrentDirectory = path
            If Len(path) >= 2 And Left(path, 2) = ".\" Then
                TrimCurrentDirectory = Mid(path, 3)
            End If
        End Function

        ''' <summary>
        ''' 3. conforming to general principles: conforming to accepted principles or standard practice
        ''' </summary>
        Public Shared Function Canonicalize(ByVal path As String) As String
            Dim sPath As String

            sPath = New String(Chr(0), MAX_PATH)

            If PathCanonicalize(sPath, path) = 0 Then
                Canonicalize = vbNullString
            Else
                Canonicalize = StripNulls(sPath)
            End If

        End Function

        ''' <summary>
        ''' Returns the most common path between two paths.
        ''' </summary>
        ''' <remarks>
        ''' <para>returns the path that is common between two paths</para>
        ''' <para>c:\FolderA\FolderB\FolderC</para>
        '''   c:\FolderA\FolderD\FolderE\File.Ext
        ''' 
        '''   results in:
        '''       c:\FolderA\
        ''' </remarks>
        Public Shared Function CommonPath(ByVal path1 As String, ByVal path2 As String) As String
            'returns the path that is common between two paths
            '
            '   c:\FolderA\FolderB\FolderC
            '   c:\FolderA\FolderD\FolderE\File.Ext
            '
            '   results in:
            '       c:\FolderA\

            Dim sResult As String = String.Empty
            Dim iPos1, iPos2 As Integer
            path1 = Canonicalize(path1)
            path2 = Canonicalize(path2)
            Do
                If Left(path1, iPos1) = Left(path2, iPos2) Then
                    sResult = Left(path1, iPos1)
                End If
                iPos1 = InStr(iPos1 + 1, path1, "\")
                iPos2 = InStr(iPos2 + 1, path1, "\")
            Loop While Left(path1, iPos1) = Left(path2, iPos2)

            Return sResult

        End Function

        Public Function CommonPath(ByVal path As String) As String
            Return CommonPath(_path, path)
        End Function

        Public Shared Function RelativePathTo(ByVal source As String, ByVal isSourceDirectory As Boolean, ByVal target As String, ByVal isTargetDirectory As Boolean) As String
            'DEVLIB
            '   05/23/05  1:47PM - Fixed call to PathRelativePathTo, iTargetAttribute is now passed to dwAttrTo instead of IsTargetDirectory.
            '       For Visual Basic 6.0, the fix does not change testing results,
            '           because when the Boolean IsTargetDirectory is converted to the Long dwAttrTo it happens to contain FILE_ATTRIBUTE_DIRECTORY,
            '
            Dim sRelativePath As String
            Dim iSourceAttribute, iTargetAttribute As Integer

            sRelativePath = New String(Chr(0), MAX_PATH)
            source = Canonicalize(source)
            target = Canonicalize(target)

            If isSourceDirectory Then
                iSourceAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If isTargetDirectory Then
                iTargetAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If PathRelativePathTo(sRelativePath, source, iSourceAttribute, target, iTargetAttribute) = 0 Then
                RelativePathTo = vbNullString
            Else
                RelativePathTo = TrimCurrentDirectory(StripNulls(sRelativePath))
            End If

        End Function

        Public Function RelativePath(ByVal target As String) As String
            Return RelativePathTo(_path, _isDirectory, target, False)
        End Function

    End Class

End Namespace

1

我在目录级别拆分了两个路径。从那里找到分歧点,然后回到组装文件夹,每次通过目录时都在前面加上一个“ ../”。

但是请记住,绝对路径无处不在,通常比相对路径更容易阅读。除非绝对必要,否则我个人不会向用户显示相对路径。


完全同意-在很多情况下,相对路径可能是完整路径名,例如,您的公共根目录是drive-c:\-因此,您仍然必须处理这种情况。
stephbu

1

如果您知道toPath由fromPath包含,则可以使其简单。为了简洁起见,我不再赘述。

public static string MakeRelativePath(string fromPath, string toPath)
{
    // use Path.GetFullPath to canonicalise the paths (deal with multiple directory seperators, etc)
    return Path.GetFullPath(toPath).Substring(Path.GetFullPath(fromPath).Length + 1);
}

2
如果它们位于不同的文件夹中怎么办?这不会附加“ ..”。如果其中一个路径已经包含“ ..”怎么办?这将返回错误的相对路径级别。如果文件A在“ MyFolder”中,文件B在“ MyLunchbox”中,该怎么办-该方法不知道目录分隔符,所以它只会认为“ Lunchbox \ File”是正确的路径。糟透了
BrainSlugs83

1

使用URI的函数返回“几乎”相对路径。它包含的目录直接包含我想要获取的相对路径的文件。

前一段时间,我编写了一个简单的函数,该函数返回文件夹或文件的相对路径,即使它在另一个驱动器上,它也包含驱动器号。

请看一下:

    public static string GetRelativePath(string BasePath, string AbsolutePath)
    {
        char Separator = Path.DirectorySeparatorChar;
        if (string.IsNullOrWhiteSpace(BasePath)) BasePath = Directory.GetCurrentDirectory();
        var ReturnPath = "";
        var CommonPart = "";
        var BasePathFolders = BasePath.Split(Separator);
        var AbsolutePathFolders = AbsolutePath.Split(Separator);
        var i = 0;
        while (i < BasePathFolders.Length & i < AbsolutePathFolders.Length)
        {
            if (BasePathFolders[i].ToLower() == AbsolutePathFolders[i].ToLower())
            {
                CommonPart += BasePathFolders[i] + Separator;
            }
            else
            {
                break;
            }
            i += 1;
        }
        if (CommonPart.Length > 0)
        {
            var parents = BasePath.Substring(CommonPart.Length - 1).Split(Separator);
            foreach (var ParentDir in parents)
            {
                if (!string.IsNullOrEmpty(ParentDir))
                    ReturnPath += ".." + Separator;
            }
        }
        ReturnPath += AbsolutePath.Substring(CommonPart.Length);
        return ReturnPath;
    }

1

如果您有一个只读文本框,是否可以不将其设置为标签并设置AutoEllipsis = true?

或者,也有一些带有代码的代码,这些代码自己生成: ,而且我还没有去寻找计算不正确的地方,如果您愿意,可以很容易地进行修改以删除目录的第一部分而不是最后一部分。

Private Function AddEllipsisPath(ByVal text As String, ByVal colIndex As Integer, ByVal grid As DataGridView) As String
    'Get the size with the column's width 
    Dim colWidth As Integer = grid.Columns(colIndex).Width

    'Calculate the dimensions of the text with the current font
    Dim textSize As SizeF = MeasureString(text, grid.Font)

    Dim rawText As String = text
    Dim FileNameLen As Integer = text.Length - text.LastIndexOf("\")
    Dim ReplaceWith As String = "\..."

    Do While textSize.Width > colWidth
        ' Trim to make room for the ellipsis
        Dim LastFolder As Integer = rawText.LastIndexOf("\", rawText.Length - FileNameLen - 1)

        If LastFolder < 0 Then
            Exit Do
        End If

        rawText = rawText.Substring(0, LastFolder) + ReplaceWith + rawText.Substring(rawText.Length - FileNameLen)

        If ReplaceWith.Length > 0 Then
            FileNameLen += 4
            ReplaceWith = ""
        End If
        textSize = MeasureString(rawText, grid.Font)
    Loop

    Return rawText
End Function

Private Function MeasureString(ByVal text As String, ByVal fontInfo As Font) As SizeF
    Dim size As SizeF
    Dim emSize As Single = fontInfo.Size
    If emSize = 0 Then emSize = 12

    Dim stringFont As New Font(fontInfo.Name, emSize)

    Dim bmp As New Bitmap(1000, 100)
    Dim g As Graphics = Graphics.FromImage(bmp)

    size = g.MeasureString(text, stringFont)
    g.Dispose()
    Return size
End Function

0
    public static string ToRelativePath(string filePath, string refPath)
    {
        var pathNormalized = Path.GetFullPath(filePath);

        var refNormalized = Path.GetFullPath(refPath);
        refNormalized = refNormalized.TrimEnd('\\', '/');

        if (!pathNormalized.StartsWith(refNormalized))
            throw new ArgumentException();
        var res = pathNormalized.Substring(refNormalized.Length + 1);
        return res;
    }

0

这应该工作:

private string rel(string path) {
  string[] cwd  = new Regex(@"[\\]").Split(Directory.GetCurrentDirectory());
  string[] fp   = new Regex(@"[\\]").Split(path);

  int common = 0;

  for (int n = 0; n < fp.Length; n++) {
    if (n < cwd.Length && n < fp.Length && cwd[n] == fp[n]) {
      common++;
    }
  }

  if (common > 0) {
    List<string> rp = new List<string>();

    for (int n = 0; n < (cwd.Length - common); n++) {
      rp.Add("..");
    }

    for (int n = common; n < fp.Length; n++) {
      rp.Add(fp[n]);
    }

    return String.Join("/", rp.ToArray());
  } else {
    return String.Join("/", fp);
  }
}

0

使用Uri的方式在linux / macOS系统上不起作用。路径“ / var / www / root”无法转换为Uri。更通用的方式-手动完成所有操作。

public static string MakeRelativePath(string fromPath, string toPath, string sep = "/")
{
    var fromParts = fromPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);
    var toParts = toPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);

    var matchedParts = fromParts
        .Zip(toParts, (x, y) => string.Compare(x, y, true) == 0)
        .TakeWhile(x => x).Count();

    return string.Join("", Enumerable.Range(0, fromParts.Length - matchedParts)
        .Select(x => ".." + sep)) +
            string.Join(sep, toParts.Skip(matchedParts));
}        

PS:我使用“ /”作为分隔符而不是Path.DirectorySeparatorChar的默认值,因为此方法的结果在我的应用中用作uri。


0

这是我的:

public static string RelativePathTo(this System.IO.DirectoryInfo @this, string to)
{
    var rgFrom = @this.FullName.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var rgTo = to.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var cSame = rgFrom.TakeWhile((p, i) => i < rgTo.Length && string.Equals(p, rgTo[i])).Count();

    return Path.Combine(
        Enumerable.Range(0, rgFrom.Length - cSame)
        .Select(_ => "..")
        .Concat(rgTo.Skip(cSame))
        .ToArray()
    );
}

0

玩类似:

private String GetRelativePath(Int32 level, String directory, out String errorMessage) {
        if (level < 0 || level > 5) {
            errorMessage = "Find some more smart input data";
            return String.Empty;
        }
        // ==========================
        while (level != 0) {
            directory = Path.GetDirectoryName(directory);
            level -= 1;
        }
        // ==========================
        errorMessage = String.Empty;
        return directory;
    }

并测试一下

[Test]
    public void RelativeDirectoryPathTest() {
        var relativePath =
            GetRelativePath(3, AppDomain.CurrentDomain.BaseDirectory, out var errorMessage);
        Console.WriteLine(relativePath);
        if (String.IsNullOrEmpty(errorMessage) == false) {
            Console.WriteLine(errorMessage);
            Assert.Fail("Can not find relative path");
        }
    }

0

在中ASP.NET Core 2,如果需要相对路径,bin\Debug\netcoreapp2.2可以使用以下组合:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
public class RenderingService : IRenderingService
{

    private readonly IHostingEnvironment _hostingEnvironment;
    public RenderingService(IHostingEnvironment hostingEnvironment)
    {
    _hostingEnvironment = hostingEnvironment;
    }

    public string RelativeAssemblyDirectory()
    {
        var contentRootPath = _hostingEnvironment.ContentRootPath;
        string executingAssemblyDirectoryAbsolutePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        string executingAssemblyDirectoryRelativePath = System.IO.Path.GetRelativePath(contentRootPath, executingAssemblyDirectoryAbsolutePath);
        return executingAssemblyDirectoryRelativePath;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.