[.net] 절대 경로에서 상대 경로를 얻는 방법

내 앱에는 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 솔루션에 관해서는 나중에 자유 시간에 시도해 볼 것입니다.



답변

.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;
}


답변

질문에 조금 늦었지만이 기능도 필요했습니다. 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);


답변

.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 답변

상대 경로를 찾는 데 사용할 수있는 PathRelativePathToA 라는 Windows API 가 있습니다. 함수에 전달하는 파일 또는 디렉토리 경로가 있어야 작동합니다.

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);
}


답변

shlwapi.dll에는 Win32 (C ++) 함수가 있으며 원하는 기능을 정확하게 수행합니다. PathRelativePathTo()

P / Invoke 이외의 .NET에서 액세스 할 수있는 방법을 모르겠습니다.


답변

.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 

그렇지 않으면 v4.7부터 .NET 전체 프레임 워크의 경우 다른 제안 된 답변 중 하나를 사용하는 것이 좋습니다.


답변

나는 이것을 과거에 사용했습니다.

/// <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;
}


답변

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는이 기능이 가장 먼저 필요하지 않으며이 작업을 수행하는 표준 기능이 있음을 나타내는 답변을 작성했습니다. 그걸 써. 그러나 첫 번째 원칙을 통해 자신의 길을 생각할 수있는 데에는 아무런 해가 없습니다.

유닉스 시스템이 드라이브 문자를 지원하지 않는다는 점을 제외하고 (모든 것이 항상 동일한 루트 디렉토리 아래에 있으므로 첫 번째 글 머리 기호는 관련이 없음) 유닉스에서도 동일한 기술을 사용할 수 있습니다.