Jul 16
The Microsoft .NET Framework is quite comprehensive, but occasionally an obvious function slips through the cracks and you have to use InteropServices to access the Windows API.
One such obvious miss is the ability to truncate a file path. If you are drawing text and know the font and desired output size, you can use the WinForms TextRenderer class. But to truncate a file path to a specific number of characters, you need the “Shell Lightweight Utility Library” function PathCompactPathEx:
using System.Runtime.InteropServices; [DllImport( "shlwapi.dll" )] static extern bool PathCompactPathEx( [Out] StringBuilder pszOut, string szPath, int cchMax, int dwFlags ); static string TruncatePath( string path, int length ) { StringBuilder sb = new StringBuilder(); PathCompactPathEx( sb, path, length, 0 ); return sb.ToString(); }
For example, here is a file path truncated to 40 characters:
C:Program FilesMicrosoft Visual Studio 8SDKv2.0Bingacutil.exe
C:Program FilesMicroso…gacutil.exe
Hello Timm,
It seems you underestimate the .NET framework a little bit. 😉
Have you ever tried this method TextRenderer.MeasureText?
You can find a detailed description on the MSDN. I reckon it does exactly the trick you mentioned above.
Cheers,
Michael
Hi Michael, thanks for commenting. I should have been more clear. If you are trying to restrict to a size on the screen with a given font, then you can use TextRenderer.MeasureText with TextFormatFlags.PathEllipsis. But if you need to restrict a file path to a specific number of characters, then you need PathCompactPathEx as described in this article.
Hi, although the interop version looks like the viable solution to my problem, I find it un”.NET” way to code. Now, is there an alternate System.Web class for TextRenderer so that I don’t have to reference System.Windows.Forms namespace on my ASPX pages/ web controls? I’m trying to build a custom datagrid with resizable column widths that require this functionality. Any advice will really help me, thanks.
Hi Chris,
The .NET Framework is essentially a collection of classes and methods that wrap interop functions, so it’s certainly not “un-.NET” for you to use interop to access functionality that the .NET designers overlooked. Interop does not introduce any unsafe code, and you’re simply using the tools you need to get the job done.
I’m unaware of a System.Web equivalent of the TextRenderer class. Could you perhaps use the Graphics class DrawString method with a StringFormat argument and StringTrimming enumeration?
Thanks for the info. I used the following to truncate a pathname with ellipsis to fit into a textbox. This method truncates the name based on the size of a textbox, not a fixed number of characters.
string fullName = @”C:UsersXY3DocumentsDates.exe”;
// Change copy of fullname to ellipsis truncated form
// and display it
string truncName = fullName.Trim();
TextRenderer.MeasureText(truncName, textBoxTruncName.Font, new Size(textBoxTruncName.Width, textBoxTruncName.Height), TextFormatFlags.ModifyString | TextFormatFlags.PathEllipsis);
textBoxTruncName.Text = truncName;
This has been a useful post for me. I see your aim, timm, which is fitting text into a defined number of characters instead of a defined space.
To solve the problem of fitting into a defined space, I am most intrigued by Gary Montante’s post (and which Michael seems to suggest), as it seems not to involve the drawText or on paint approaches I have seen elsewhere.
I am quite new to C# and .NET, though an old C programmer, so I am thrown a bit by something very basic: It appears TextRenderer,MeaureText is not only measuring the text, but is actually modifying it (or replacing the text which truncName was pointing to). I don’t see this documented in the MS help for MeasureText, though Michael’s post alludes to it. Does anyone have a reference to that behavior?
Thanks,
Mike
Hi, Mike,
Take a look at
http://www.codeproject.com/KB/vb/NewPathCompactPath.aspx
which has some explanation of TextFormatFlags.ModifyString.
Sorry, but I don’t remember exactly how/where I stumbled onto the solution. My usual fact finding mode is to Google suggestive words, read some, search some more, and experiment with code when/if I see something that seems relevant.
I, too, was an old Fortran, Assembler, C/C++ programmer (since the late 1970s) and high school/community college instructor until a friend introduced me to C#. Well, better late than never! At least I got to see what heaven could be like while I’m still alive!
Gary
P.S. I recommend learning C# in a guided manner. The C# language has changed since I formally learned it, so I’m not the best source for a recommendation on new textbooks. But, in the past, I found the Microsoft C# Language document (e.g., http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/csharp%20language%20specification.doc) quite good for the language specifics. However, for creating Forms programs, a textbook like “Murach’s C#”; “Windows Forms Programming in C#” (Chris Sells), or “Microsoft Visual C#.NET Step by Step” (John Sharp, Jon Jagger) to be very useful. Once you got the hang of it, you might like a guide to find quicky info like Microsoft’s “C# Programmer’s Cookbook”. Happy programming…
Gary,
Thanks. I think that codeproject link is about as explicit as it gets about the behavior of MeasureText. As it says, “it’s relatively undocumented”.
I found a suggestion by a Craig Murphy similar to yours at http://social.msdn.microsoft.com/Forums/en-US/winforms/thread/1acc1d42-416e-4a10-9112-11018e611d95/.
And thanks for the comments on learning C#. I kind of spent the 90’s avoiding C++, so I am long overdue for a rollover in my knowledge base.
Relative to our offline e-mail exchange, and perhaps for the comment and benefit of others, I had adapted this approach to work with a filename obtained via openFileDialog.Filename. Being a noob, I think I have misunderstood how strings are handled in C#. For it appears that the transformation to the truncated path appears not only in truncString but in openFileDialog.Filename. The sign of trouble was an obscure (to me) exception after calling MeasureText. The VS2008 debugger shows ‘((System.Windows.Forms.FileDialog)(openFileDialogBMP)).FileName’ threw an exception of type ‘System.ArgumentException’ for the Filename value in the watch window. My working theory, though I’ve not proved it yet, is that the ellipsis gets into the Filename field (due to my misunderstanding of string references) and that is illegal in a filename.
Anyway, I hope to figure this out fully and will report the result here.
Mike
Here is a sample class that encapsulates an ellipsis textbox.
Sample usage:
EllipsisTextBox txtBox = new EllipsisTextBox();
txtBox.EllipsisType =
EllipsisTextBox.EllipsisLocation.Path;
txtBox.Text =
@”C:directorysubdirectoryfilename.ext”;
Depending on the width of the textbox, it will display something like “C:di…filename.ext”.
However,
string boo = txtBox.Text; yields
boo == @”C:directorysubdirectoryfilename.ext”, the original string, NOT the truncated string.
The sample textbox can be dragged onto a form, and assigned EllipsisType in the Properties window.
The sample also demonstrates a “bug” with String.Trim().
=================================================
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace MyProject
{
///
/// Creates a TextBox with built-in ellipsis control.
/// (1) Specify EllipsisTextBox.EllipsisType
/// (e.g., = EllipsisLocation.Path)
/// (2) Put text into the textbox (e.g., EllipsisTextBox.Text = @”c:directoryfile.txt”)
/// The displayed text is the original text modified by the EllipsisType.
/// (e.g., “…file.txt” if that’s what fills the small textbox)
/// The string returned by EllipsisTextBox.Text is the original text,
/// which may differ from what is showing in the textbox.
///
public partial class EllipsisTextBox : System.Windows.Forms.TextBox
{
private string fullText;
private string ellipsisText;
private EllipsisLocation ellipsisLocation = EllipsisLocation.None;
private TextFormatFlags ellipsisTextFormatFlag = 0;
public enum EllipsisLocation { Path, Word, End, None };
///
/// Define where ellipsis appears
///
/// TextFormatFlags.PathEllipsis,TextFormatFlags.EndEllipsis,TextFormatFlags.WordEllipsis
///
[System.ComponentModel.Description(“Define where ellipsis appears in the string”)]
[System.ComponentModel.DefaultValue(EllipsisLocation.None)]
public EllipsisLocation EllipsisType
{
get { return ellipsisLocation; }
set {
ellipsisLocation = value;
switch (value)
{
case EllipsisLocation.End:
ellipsisTextFormatFlag = TextFormatFlags.EndEllipsis; break;
case EllipsisLocation.None:
ellipsisTextFormatFlag = 0; break;
case EllipsisLocation.Path:
ellipsisTextFormatFlag = TextFormatFlags.PathEllipsis; break;
case EllipsisLocation.Word:
ellipsisTextFormatFlag = TextFormatFlags.WordEllipsis; break;
default:
ellipsisTextFormatFlag = 0;
ellipsisLocation = EllipsisLocation.None;
break;
}
}
}
///
/// Modify/Get text of an EllipseTextBox.
/// Get: returns the original, unmodified assigned text.
/// Set: saves a copy of the new text, but displays
/// the new text as modified by EllipsisType.
///
public new string Text
{
get
{
return fullText;
}
set
{
// Ensure we get a trimmed copy of the original string,
// NOT a reference to the original string.
// NOTE: seems to be a “bug” in .Trim() – returns a reference
// to the original string if .Trim() does not need to modify
// the original string, otherwise, it returns a reference to a new string.
// We could use…
// string truncText = (new StringBuilder(value)).ToString();
// but drop the below to see the .Trim() anomaly!
fullText = (value + ” “).Trim(); //want a new copy, not a reference to value!
// Change copy of fullText to ellipsis truncated form
//truncText = (new StringBuilder(value)).ToString();
ellipsisText = (fullText + ” “).Trim(); //want a new copy to modify, else fullText would be modified, too!
TextRenderer.MeasureText(ellipsisText, this.Font, new Size(this.Width, this.Height), TextFormatFlags.ModifyString | ellipsisTextFormatFlag);
base.Text = ellipsisText;
}
}
public EllipsisTextBox() : base()
{
fullText = “”;
base.Text = “”;
}
}
}
@Gary Montante Thanks; here is the functionized version:
using System.Drawing;
using System.Windows.Forms;
public string TruncateFileName( string fileName, TextBox textBox )
{
string truncName = String.Copy(fileName);
TextRenderer.MeasureText(
truncName,
textBox.Font,
new Size(textBox.Width, textBox.Height),
TextFormatFlags.ModifyString | TextFormatFlags.PathEllipsis);
return truncName;
}
Replying in reference to “another mike”‘s comment above converning the strange string when using the Measuretext method to truncate a string, I created the following extension method for the FileIno class to return the truncated path:
public static string TruncatedFullName(this FileInfo f, System.Drawing.Font font, int width)
{
string truncatedPath = (String)f.FullName.Trim();
System.Windows.Forms.TextRenderer.MeasureText(truncatedPath, font, new System.Drawing.Size(Convert.ToInt32(width), Convert.ToInt32(font.GetHeight())), System.Windows.Forms.TextFormatFlags.ModifyString | System.Windows.Forms.TextFormatFlags.PathEllipsis);
return truncatedPath;
}
Before the MeasureText method is called, both the f.FullName and truncatedPath variables are correctly displaying the path to the file, as verified by the watch window
C:Documents and SettingsmynameMy DocumentstestingDaffy_Duck.xml
After the MeasureText method is called, I receive the error
‘f.FullName’ threw an exception of type ‘System.ArgumentException’
and the trucatedPath variable reads
C:\Documents an…\Daffy_Duck.xml\My Documents\testing\Daffy_Duck.xml
which is strange, because when I view the reported value of truncatedPath, it reads
C:\Documents an…\Daffy_Duck.xml
as expected. I am assuming the nullcharacter in the string is what is doing the truncating of the value in this instance. What also surpriss me is the the value of the f.FullName property of the FileInfo object is also changing.
I have no solution for this problem, and any help would be appreciated. Thanks
In the above post, the formatting left out a null character
between the trucated file name and My Documents, so if html tags can be placed here it should read
C:\Documents an…\Daffy_Duck.xmlMy Documents\testing\Daffy_Duck.xml
You need to use
string truncatedPath = String.Copy(f.FullName.Trim());
You experienced the same error I did which I described extensively in my sample class.
In your case, .Trim() did not allocate a new string. Instead, it just returned a reference to the string f.FullName. Subsequently, .MeasureText() shortens f.FullName creating an “illegal” name for FileInfo.
Brian Tyler’s use of String.Copy() fixes the problem (thanks, Brian). It creates a new string independent of FileInfo, which can be changed anyway you wish without messing up FileInfo.
Re: Subsequently, .MeasureText() shortens f.FullName creating an “illegal” name for FileInfo.
Perhaps I am misunderstanding what you mean, but strings are immutable. Calling f.FullName.Trim() or MeasureText() or whatever will not modify the original f.FullName string. The only way to change f.FullName string is to assign it to a different string, but the original string remains unchanged.
The only time you need to use String.Copy in .NET is “with unmanaged code that deals with the memory locations directly and can mutate the string.”
http://stackoverflow.com/questions/520895/whats-the-use-of-system-string-copy-in-net
Hi, Simon,
Thanks for your info. Interesting url.
Did you experiment doing the example with and without using String.Copy? Your comment “Calling f.FullName.Trim() or MeasureText() or whatever will not modify the original f.FullName string” suggests that you didn’t.
When you do, you’ll see that .MeasureText() appears to be a case of “unmanaged code that deals with the memory locations directly and can mutate the string.” The circumstantial evidence looks like .MeasureText() did not allocate a new string and return a reference to it, but instead put a “null” into the original input string (“argument exception, illegal characters path, null”).
My conclusion is that this is screwy stuff and works in an unexpected, un-.Net manner.
But, ultimately the point was to fix some code that gave an exception.
To do this, I re-read the .MeasureText() documentation, which didn’t show me what I did wrong (I always assume that it was something I did wrong or didn’t understand). That didn’t work.
If I were still a professional programmer, I would try to examine the library code to see what it was actually doing, not what some documentation says it should be doing. “Go to disassembly” didn’t work (incidentally, I have found compiler bugs and library code bugs this way…) and I didn’t want to take the time to use the excellent tool described at
http://en.csharp-online.net/Visual_Studio_Hacks%E2%80%94Hack_64:_Examine_the_Innards_of_Assemblies .
I guessed the problem was modification of the original string by .MeasureText() so I tried to create a second copy. To do this, I “played monkey” by trying whatever I could think of (I originally used .Trim() which created a new copy only if it did something, otherwise it didn’t; then forced .Trim() to do something, which worked but was unsatisfying; then played with StringBuilder, which worked; then liked and used the info about String.Copy). Problem solved, work on something else.
I feel that whoever worked on the .MeasureText() did not take the time to create a function that played nicely with .Net strings, which could have easily been avoided by creating a separate playground for the underlying code, and would have saved us all a bunch of time.
I’m guessing the wrapper passes memory addresses to the underlying “C++” code, which uses the pointer to change memory directly. Since there is no garbage collection in C++, this was a legimate way to use a character array and avoid a memory leak.
Sincerely,
Gary
Just found these comments while trying to figure out a way to get the actual characters of the string that gets drawn when using the DrawString method and a StringFormat specifying the EllipsisPath trimming option.
I’m doing some owner-draw logic for a listview that is looping through a list of strings and identifying matches for a regular expression search, and then drawing the strings a piece at a time, highlighting the search matches.
The original draw logic used DrawString and the EllipsisPath trimming option, and I needed to get the actual text of the string being drawn so I could do the regex match on the ellipsified string, since that is the string I will be drawing (the ellipsis might hide some of the matches, which requires some special handling of the displayed output).
In any case, I found this thread and couldn’t wait to try the code, since it seemed the exact answer I was looking for.
I discovered that, in VB.Net, the results of modifying the first argument string to the TextRenderer.MeasureText method resulted in the new, shorter string being terminated with a a null embedded in the string. I used the following code in VB.Net to extract the new string:
‘ First get the ellipsified string in strTrunc
Dim strTrunc As New String(strOutput)
TextRenderer.MeasureText(strTrunc, Font, New Size(Bounds.Width, Bounds.Height), TextFormatFlags.ModifyString Or TextFormatFlags.PathEllipsis)
‘Now split out the result to make it usable in VB.Net
strTrunc = strTrunc.Split(Chr(0))(0)
This is working pretty well in my code, but I have discovered that there are slight differences in the actual resulting character-strings between these two methods:
1) DrawString with EllipsisPath flag:
sf.Trimming = System.Drawing.StringTrimming.EllipsisPath
Graphics.DrawString(strOut, Font, Brush, Bounds, sf)
2) TextRenderer method:
TextRenderer.MeasureText(strTrunc, fFont, New Size(rBounds.Width, rBounds.Height), TextFormatFlags.ModifyString Or TextFormatFlags.PathEllipsis)
The results are quite similar, but not identical.
Here is an example of a string in my test data, with the resulting ellipsified text from both methods above:
Original string:
” A 2,991,428 E:UsersplmillsMusiciTunesiTunes MusicAl PettewayMidsummer Moon1 The Red Haired Boy.mp3″
Ellipsified text using DrawString & EllipsisPath (for a specific font and bounding rectangle):
” A 2,991,428 E:UsersplmillsMusiciTunesiTunes MusicAl…1 The Red Haired Boy.mp3″
Ellipsified text using TextRenderer & PathEllipsis flag (with the same font and bounding rectangle):
” A 2,991,428 E:UsersplmillsMusiciTunesiTunes Mus…1 The Red Haired Boy.mp3″
Slightly different, and not even the same number of characters in the result, which is odd since I’m using a mono-spaced font for the measuring.
Just thought you might find these results of interest. Thanks for the code in this thread – very helpful for getting me over a tough spot.
Awesome stuff. Thanks a bunch for sharing this.