Function to shrink file path to be more human readable
Asked Answered
S

9

9

Is there any function in c# to shink a file path ?

Input: "c:\users\Windows\Downloaded Program Files\Folder\Inside\example\file.txt"

Output: "c:\users\...\example\file.txt"

Shastashastra answered 2/12, 2011 at 17:41 Comment(2)
Is this for WinForms or do you just want a shorter string? (I ask because .NET supports this for drawing only, which only applies to WinForms and images).Guileful
Why would you want to take the full path out? If that were the case the user wouldn't be able to find the file. If you aren't worried about that - then don't display the path at all - just the file name.Recreate
A
6

Jeff Atwood posted a solution to this on his blog and here it is :

[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathCompactPathEx([Out] StringBuilder pszOut, string szPath, int cchMax, int dwFlags);

static string PathShortener(string path, int length)
{
    StringBuilder sb = new StringBuilder();
    PathCompactPathEx(sb, path, length, 0);
    return sb.ToString();
}

It uses the unmanaged function PathCompactPathEx to achieve what you want.

Anjelicaanjou answered 2/12, 2011 at 17:58 Comment(4)
mysteriously this was working awsome in the framework 3.5, i updated to vs 2010 and framework 4 and now I get an undebuggable crash on this line. (visual studio is busy...) Switching to CharSet.Ansi "solved" (?) the problem.Deterioration
@Deterioration It crashed for me, too, until I saw the solution of Daniele which uses another constructor: sb = new StringBuilder(length + 1).Ammon
Beware: this crashes (at least with recent .NET frameworks), because StringBuilder by default allocates 16 characters, which is often not enough to store the result. You need to use length or MAX_PATH = 260 to avoid the possible memory corruption.Brill
Atwood changed his blog domain and URL scheme. Here's an updated link: blog.codinghorror.com/shortening-long-file-pathsObey
O
10

Nasreddine answer was nearly correct. Just specify StringBuilder size, in your case:

[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathCompactPathEx(
                       [Out] StringBuilder pszOut, 
                       string szPath, 
                       int cchMax, 
                       int dwFlags);

static string PathShortener(string path, int length)
{
    StringBuilder sb = new StringBuilder(length + 1);
    PathCompactPathEx(sb, path, length, 0);
    return sb.ToString();
}
Odisodium answered 11/3, 2014 at 14:22 Comment(1)
Here's what I think is an interesting question, I tried this, replacing the [Out] attribute with the out keyword in the declaration (and adding the out keyword at the method call site to appease the compiler), but it stopped working. Why? I thought the [Out] attribute and out keyword were meant to be 100% interchangeable.First
B
6

That looks less human readable to me. Anyway, I don't think there is such a function. split it on the \ character and just keep the first two slots and the last two slots and you have it.

Something like this, although that code is not very elegant

  string[] splits = path.Split('\\');
  Console.WriteLine( splits[0] + "\\" + splits[1] + "\\...\\" + splits[splits.Length - 2] + "\\" +  splits[splits.Length - 1]);
Byler answered 2/12, 2011 at 17:44 Comment(2)
I'd also add an if (splits.Length > 4) test in there too.Ease
This is absolutely NOT shrinking the path... Think to have a path like C:\veryveryveryveryverylong\supersupersuperlong\a\b\c\d.txt ... What exactly will your solution solve?Bedrabble
A
6

Jeff Atwood posted a solution to this on his blog and here it is :

[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathCompactPathEx([Out] StringBuilder pszOut, string szPath, int cchMax, int dwFlags);

static string PathShortener(string path, int length)
{
    StringBuilder sb = new StringBuilder();
    PathCompactPathEx(sb, path, length, 0);
    return sb.ToString();
}

It uses the unmanaged function PathCompactPathEx to achieve what you want.

Anjelicaanjou answered 2/12, 2011 at 17:58 Comment(4)
mysteriously this was working awsome in the framework 3.5, i updated to vs 2010 and framework 4 and now I get an undebuggable crash on this line. (visual studio is busy...) Switching to CharSet.Ansi "solved" (?) the problem.Deterioration
@Deterioration It crashed for me, too, until I saw the solution of Daniele which uses another constructor: sb = new StringBuilder(length + 1).Ammon
Beware: this crashes (at least with recent .NET frameworks), because StringBuilder by default allocates 16 characters, which is often not enough to store the result. You need to use length or MAX_PATH = 260 to avoid the possible memory corruption.Brill
Atwood changed his blog domain and URL scheme. Here's an updated link: blog.codinghorror.com/shortening-long-file-pathsObey
M
5

If you want, do insert ellipsis dependent on the length of the path string, then use this code:

TextRenderer.MeasureText(path, Font, 
    new System.Drawing.Size(Width, 0),
    TextFormatFlags.PathEllipsis | TextFormatFlags.ModifyString);

It will modify path in-place.

EDIT: Be careful with this method. It breaks the rule, saying that strings in .NET are immutable. In fact, the first parameter of the MeasureText method is not a ref parameter, which means that no new string can be returned. Instead, the existing string is altered. It would be careful to work on a copy created with

string temp = String.Copy(path);
Millicent answered 2/12, 2011 at 17:57 Comment(0)
C
2

You could use something like:

public string ShrinkPath(string path, int maxLength)
{
    List<string> parts = new List<string>(path.Split('\\'));

    string start = parts[0] + @"\" + parts[1];
    parts.RemoveAt(1);
    parts.RemoveAt(0);

    string end = parts[parts.Count-1];
    parts.RemoveAt(parts.Count-1);

    parts.Insert(0, "...");
    while(parts.Count > 1 && 
      start.Length + end.Length + parts.Sum(p=>p.Length) + parts.Count > maxLength)
        parts.RemoveAt(parts.Count-1);

    string mid = "";
    parts.ForEach(p => mid += p + @"\");

    return start+mid+end;
}

Or just use Olivers solution, which is much easier ;-).

Ceratoid answered 2/12, 2011 at 17:57 Comment(0)
I
1

I was just faced with this issue as long paths were becoming a complete eye sore. Here is what I tossed together real quick (mind the sloppiness) but it gets the job done.

private string ShortenPath(string path, int maxLength)
{
    int pathLength = path.Length;

    string[] parts;
    parts = label1.Text.Split('\\');

    int startIndex = (parts.Length - 1) / 2;
    int index = startIndex;

    string output = "";
    output = String.Join("\\", parts, 0, parts.Length);

    decimal step = 0;
    int lean = 1;

    do
    {
        parts[index] = "...";

        output = String.Join("\\", parts, 0, parts.Length);

        step = step + 0.5M;
        lean = lean * -1;

        index = startIndex + ((int)step * lean);
    }
    while (output.Length >= maxLength && index != -1);

    return output;
}

Results

EDIT

Below is an update with Merlin2001's corrections.

private string ShortenPath(string path, int maxLength)
{
    int pathLength = path.Length;

    string[] parts;
    parts = path.Split('\\');

    int startIndex = (parts.Length - 1) / 2;
    int index = startIndex;

    String output = "";
    output = String.Join("\\", parts, 0, parts.Length);

    decimal step = 0;
    int lean = 1;

    while (output.Length >= maxLength && index != 0 && index != -1)
    {
        parts[index] = "...";

        output = String.Join("\\", parts, 0, parts.Length);

        step = step + 0.5M;
        lean = lean * -1;

        index = startIndex + ((int)step * lean);
    }
    // result can be longer than maxLength
    return output.Substring(0, Math.Min(maxLength, output.Length));  
}
Isahella answered 19/9, 2015 at 4:26 Comment(8)
Clever idea! But you always replace at least one part of the path even if it would fit in the total length. And also you can run into an IndexOutOfRangeException if the path doesn't fit even with all parts replaced by .... To fix this (and keep at least the drive letter and the last directory) I would suggest changing the do..while to a while and check for index != 0 && index < parts.Length - 1 instead of just index != -1. This way you don't run into exceptions and don't add superfluous ellipses.Kilter
@Merlin2001 Thank you very much and good catch! You're suggestions will definitely improve my code. I'll be sure to make the appropriate updates.Isahella
You should promote using type aliases, instead of using the type names. So int instead of Int32Boothman
Sorry, I just prefer it probably due to my background and personal preferences. Old habits die hard. For the sake of argument I will do my best when posting code snippets to comply with majority expectations.Isahella
Edited to allow only maxLength characters (it can break at a bad point, but it won't exceed caller's specified length)Cortese
@J.ChrisCompton, your edit appears to be 100% valid but it would really help if you could provide a specific example that it fixesRizo
@ChrisHaas Example where Substring is needed: ShortenPath(@"C:\Temp\A folder\B folder\C folder\D folder\E folder\", 20) without the Substring the result is 35 characters C:\...\...\...\...\...\...\F folderCortese
This is the wrong way. You do not take into account the width of each character. 30 "W" characters have the same witdh as 90 "i" characters, Additionally it depends on the Font that you use.Cymatium
I
1
    private string ShrinkPath(string path, int maxLength)
    {
        var parts = path.Split('\\');
        var output = String.Join("\\", parts, 0, parts.Length);
        var endIndex = (parts.Length - 1);
        var startIndex = endIndex / 2;
        var index = startIndex;
        var step = 0;

        while (output.Length >= maxLength && index != 0 && index != endIndex)
        {
            parts[index] = "...";
            output = String.Join("\\", parts, 0, parts.Length);
            if (step >= 0) step++;
            step = (step * -1);
            index = startIndex + step;
        }
        return output;
    }
Intussuscept answered 2/5, 2017 at 20:48 Comment(1)
Beautiful code. Also very easy to modify to work with a pixel width and MeasureString. Thanks.Butlery
C
1

Nearly all answers here shorten the path string by counting characters. But this approach ignores the width of each character.

These are 30 'W' characters:

WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW

These are 30 'i' characters:

iiiiiiiiiiiiiiiiiiiiiiiiiiiiii

As you see, counting characters is not really useful.

And there is no need to write your own code because the Windows API has this functionaly since Windows 95. The name of this functionality is "Path Ellipsis". The Windows API DrawTextW() has a flag DT_PATH_ELLIPSIS which does exactly this. In the .NET framwork this is available (without the need to use PInvoke) in the TextRenderer class.

There are 2 ways how this can be used:


1.) Drawing the path directly into a Label:

public class PathLabel : Label
{
    protected override void OnPaint(PaintEventArgs e)
    {
        if (AutoSize)
            throw new Exception("You must set "+Name+".AutoSize = false in VS " 
                              + "Designer and assign a fix width to the PathLabel.");

        Color c_Fore = Enabled ? ForeColor : SystemColors.GrayText;
        TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, c_Fore, 
                              BackColor, TextFormatFlags.PathEllipsis);
    }
}

This label requires you to turn AutoEllipsis off in Visual Studio Designer and assign a fix width to the Label (the maximum width that your path should occupy).

You even see the truncated path in Visual Studio Designer.

I entered a long path which does not fit into the label:

C:\WINDOWS\Installer{40BF1E83-20EB-11D8-97C5-0009C5020658}\ARPPRODUCTICON.exe

Even in Visual Studio Designer it is displayed like this:

Label with Path Ellipsis in C#


2.) Shorten the path without drawing it on the screen:

public static String ShortenPath(String s_Path, Font i_Font, int s32_Width)
{
    TextRenderer.MeasureText(s_Path, i_Font, new Size(s32_Width, 100), 
                             TextFormatFlags.PathEllipsis | TextFormatFlags.ModifyString);

    // Windows inserts a '\0' character into the string instead of shortening the string
    int s32_Nul = s_Path.IndexOf((Char)0);
    if (s32_Nul > 0)
        s_Path = s_Path.Substring(0, s32_Nul);
    return s_Path;
}

The flag TextFormatFlags.ModifyString inserts a '\0' character into the string. It is very unusual that a string is modified in C#. Normally strings are unmutable. This is because the underlying API DrawTextW() works this way. But as the string is only shortened and never will become longer there is no risk of a buffer overflow.

The following code

String s_Text = @"C:\WINDOWS\Installer{40BF1E83-20EB-11D8-97C5-0009C5020658}\ARPPRODUCTICON.exe";
s_Text = ShortenPath(s_Text, new Font("Arial", 12), 500);

will result in "C:\WINDOWS\Installer{40BF1E83-20EB-1...\ARPPRODUCTICON.exe"

Cymatium answered 7/6, 2021 at 18:28 Comment(0)
S
0

If you want to write you own solution to this problem, use build in classes like: FileInfo, Directory, etc... which makes it less error prone.

The following code produces "VS style" shortened path like: "C:\...\Folder\File.ext".

public static class PathFormatter
{
    public static string ShrinkPath(string absolutePath, int limit, string spacer = "…")
    {
        if (string.IsNullOrWhiteSpace(absolutePath))
        {
            return string.Empty;
        }
        if (absolutePath.Length <= limit)
        {
            return absolutePath;
        }

        var parts = new List<string>();

        var fi = new FileInfo(absolutePath);
        string drive = Path.GetPathRoot(fi.FullName);

        parts.Add(drive.TrimEnd('\\'));
        parts.Add(spacer);
        parts.Add(fi.Name);

        var ret = string.Join("\\", parts);
        var dir = fi.Directory;

        while (ret.Length < limit && dir != null)
        {
            if (ret.Length + dir.Name.Length > limit)
            {
                break;
            }

            parts.Insert(2, dir.Name);

            dir = dir.Parent;
            ret = string.Join("\\", parts);
        }

        return ret;
    }
}
Screeching answered 11/7, 2018 at 9:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.