8

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"

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
André Pontes
  • 447
  • 1
  • 4
  • 13
  • 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). – Scott Rippey Dec 02 '11 at 18:08
  • 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. – tsells Dec 02 '11 at 19:34

9 Answers9

9

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();
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Daniele
  • 191
  • 1
  • 4
  • 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. – Geoff Jan 13 '22 at 18:11
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]);
Orn Kristjansson
  • 3,435
  • 4
  • 26
  • 40
  • 2
    I'd also add an `if (splits.Length > 4)` test in there too. – LukeH Dec 02 '11 at 18:06
  • 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? – garlix Mar 28 '19 at 13:39
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.

Nasreddine
  • 36,610
  • 17
  • 75
  • 94
  • 3
    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. – v.oddou Nov 18 '13 at 07:21
  • 4
    @v.oddou It crashed for me, too, until I saw [the solution of Daniele](http://stackoverflow.com/a/22328545/107625) which uses another constructor: `sb = new StringBuilder(length + 1)`. – Uwe Keim Aug 07 '14 at 12:55
  • 2
    Beware: this crashes (at least with recent .NET frameworks), because `StringBuilder` [by default allocates 16 characters](http://stackoverflow.com/questions/246211/default-capacity-of-stringbuilder), which is often not enough to store the result. You need to use length or MAX_PATH = 260 to avoid the possible memory corruption. – Suma Aug 27 '15 at 15:53
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);
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
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 ;-).

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Christoph Fink
  • 22,727
  • 9
  • 68
  • 113
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));  
}
J. Chris Compton
  • 538
  • 1
  • 6
  • 25
David Carrigan
  • 751
  • 1
  • 8
  • 21
  • 1
    **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. – Marcus Mangelsdorf Oct 21 '15 at 15:11
  • @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. – David Carrigan Oct 21 '15 at 15:42
  • You should promote using type aliases, instead of using the type names. So `int` instead of `Int32` – Germstorm Dec 16 '15 at 08:04
  • 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. – David Carrigan Dec 16 '15 at 16:14
  • Edited to allow only maxLength characters (it can break at a bad point, but it won't exceed caller's specified length) – J. Chris Compton Nov 05 '19 at 18:58
  • @J.ChrisCompton, your edit appears to be 100% valid but it would really help if you could provide a specific example that it fixes – Chris Haas Nov 05 '19 at 21:55
  • @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 folder` – J. Chris Compton Nov 06 '19 at 17:28
  • 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. – Elmue May 20 '21 at 15:02
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;
    }
  • Beautiful code. Also very easy to modify to work with a pixel width and `MeasureString`. Thanks. –  Aug 26 '17 at 23:42
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;
    }
}
Major
  • 5,948
  • 2
  • 45
  • 60
0

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"

Elmue
  • 7,602
  • 3
  • 47
  • 57