Click here to Skip to main content
Click here to Skip to main content

Auto Ellipsis

, 20 Jun 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
Add "Auto Ellipsis" feature to any Windows Form control
demo

Introduction

Why yet another ellipsis control, when the .NET Framework already provides several built-in options to achieve this task? System.Windows.Forms.Label control comes with an AutoEllipsis property. System.Drawing.Graphics.DrawString or System.Windows.Forms.TextRenderer.DrawText offer a reliable way to make text fit into predefined boundaries. Just have a look at StringTrimming or TextFormatFlags enumeration! Not to mention PathCompactPath API from shlwapi.dll or Static control styles SS_ENDELLIPSIS and SS_PATHELLIPSIS.

Unfortunately, the built-in auto ellipsis controls provide no flexibility at all for ellipsis alignment. The text is always trimmed off at the end the string. This might be an issue, such as in the following example:

C:\Documents and Settings\TPOL\My Documents\Visual Studio 2005\
				Projects\MyProject1\Program.cs
C:\Documents and Settings\TPOL\My Documents\Visual Studio 2005\
				Projects\MyProject2\Program.cs

The built-in auto ellipsis controls display paths as follows:

C:\Documents and Settings\TPOL\My Documents\Visual...\Program.cs
C:\Documents and Settings\TPOL\My Documents\Visual...\Program.cs

It would be helpful to keep the last part of the path as it is more significant in this case.

C:\...Documents\Visual Studio 2005\Projects\MyProject1\Program.cs
C:\...Documents\Visual Studio 2005\Projects\MyProject2\Program.cs

By the way, Visual Studio 2005 behaves like this in the "File/Recent Files" menu.

Using the Code

This is why I came up with the Ellipsis class. It is a static class with a single method:

public static string Compact(string text, Control ctrl, EllipsisFormat options)

The Compact function trims off argument text to make it fit into ctrl boundaries. EllipsisFormat enumeration is defined as follows:

[Flags]
public enum EllipsisFormat
{
	// Text is not modified.
	None = 0,
	// Text is trimmed at the end of the string. An ellipsis (...) 
	// is drawn in place of remaining text.
	End = 1,
	// Text is trimmed at the beginning of the string. 
	// An ellipsis (...) is drawn in place of remaining text. 
	Start = 2,
	// Text is trimmed in the middle of the string. 
	// An ellipsis (...) is drawn in place of remaining text.
	Middle = 3,
	// Preserve as much as possible of the drive and filename information. 
	// Must be combined with alignment information.
	Path = 4,
	// Text is trimmed at a word boundary. 
	// Must be combined with alignment information.
	Word = 8
}

The Ellipsis class can be used to implement flexible auto ellipsis on various Windows Form controls. I provided two examples in the demo project, one for Label control, one for TextBox control.

TextBoxEllipsis

The TextBoxEllipsis switches to "full text" mode when it gains focus so its content can be edited as usual. It switches back to "ellipsis" mode when it loses focus.

Inside the code

Find the Correct Size: The Bisection Method

A working ellipse algorithm should find the longest substring that can fit into the control boundaries. The brute force approach would test all substrings by removing characters one by one. The proposed solution uses the bisection method to minimize the number of iterations to get the closest match.

Bisection example:

Bisection method

The algorithm uses the TextRenderer.MeasureText method to get the size, in pixels, of the specified text drawn on the specified control (using the control's font). The bisection method is implemented as follows (some code has been removed for clarity):

public static readonly string EllipsisChars = "...";

public static string Compact(string text, Control ctrl, EllipsisFormat options)
{
	using (Graphics dc = ctrl.CreateGraphics())
	{
		Size s = TextRenderer.MeasureText(dc, text, ctrl.Font);

		// control is large enough to display the whole text 
		if (s.Width <= ctrl.Width)
			return text;

		int len = 0;
		int seg = text.Length;
		string fit = "";

		// find the longest string that fits into
		// the control boundaries using bisection method 
		while (seg > 1)
		{
			seg -= seg / 2;

			int left = len + seg;
			int right = text.Length;

			if (left > right)
				continue;

			if ((EllipsisFormat.Middle & options) == 
						EllipsisFormat.Middle)
			{
				right -= left / 2;
				left -= left / 2;
			}
			else if ((EllipsisFormat.Start & options) != 0)
			{
				right -= left;
				left = 0;
			}

			// build and measure a candidate string with ellipsis
			string tst = text.Substring(0, left) + 
				EllipsisChars + text.Substring(right);
			
			s = TextRenderer.MeasureText(dc, tst, ctrl.Font);

			// candidate string fits into control boundaries, 
			// try a longer string
			// stop when seg <= 1 
			if (s.Width <= ctrl.Width)
			{
				len += seg;
				fit = tst;
			}
		}

		if (len == 0) // string can't fit into control
		{
			return EllipsisChars;
		}
		return fit;
	}
}

The Compact method in action:

Ellipsis algorithm

Trim at a Word Boundary using Regular Expressions

The .NET Framework allows to trim text at a word boundary. We implement it by adjusting the substring bounds with regular expressions:

  • "\w*\W*" matches a word followed by whitespaces
  • "\W*\w*$" matches whitespaces followed by a word at the end of the string

These matches are subtracted from the substring (according to ellipsis alignment) in order to round up text at a word boundary.

private static Regex prevWord = new Regex(@"\W*\w*$");
private static Regex nextWord = new Regex(@"\w*\W*");

public static string Compact(string text, Control ctrl, EllipsisFormat options)
{
	using (Graphics dc = ctrl.CreateGraphics())
	{
		[..] 

		int len = 0;
		int seg = text.Length;
		string fit = "";

		// find the longest string that fits into
		// the control boundaries using bisection method 
		while (seg > 1)
		{
			seg -= seg / 2;

			int left = len + seg;
			int right = text.Length;

			[..]

			// trim at a word boundary using regular expressions 
			if ((EllipsisFormat.Word & options) != 0)
			{
				if ((EllipsisFormat.End & options) != 0)
				{
					left -= prevWord.Match(text, 
							0, left).Length;
				}
				if ((EllipsisFormat.Start & options) != 0)
				{
					right += nextWord.Match(text, 
							right).Length;
				}
			}
			
			// build and measure a candidate string with ellipsis
			string tst = text.Substring(0, left) + 
				EllipsisChars + text.Substring(right);
			
			s = TextRenderer.MeasureText(dc, tst, ctrl.Font);

			// candidate string fits into control boundaries, 
			// try a longer string
			// stop when seg <= 1 
			if (s.Width <= ctrl.Width)
			{
				len += seg;
				fit = tst;
			}
		}

		if (len == 0) // string can't fit into control
		{
			return EllipsisChars;
		}
		return fit;
	}
}

Example of text trimmed at a word boundary:

Trim at a word boundary

Trim a Path String

The "path" mode is a feature where the specified text is handled as a file path. The algorithm preserves as much as possible of the drive and filename information:

  1. c:\directory1\dir...\filename.ext
  2. c:\...\filename.ext
  3. ...\filename.ext (this is the shortest possible path, filename and extension are not truncated).
public static string Compact(string text, Control ctrl, EllipsisFormat options)
{
	using (Graphics dc = ctrl.CreateGraphics())
	{
		[..]
		
		string pre = "";
		string mid = text;
		string post = "";

		bool isPath = (EllipsisFormat.Path & options) != 0;

		// split path string into <drive><directory><filename> 
		if (isPath)
		{
			pre = Path.GetPathRoot(text);
			mid = Path.GetDirectoryName(text).Substring(pre.Length);
			post = Path.GetFileName(text);
		}

		int len = 0;
		int seg = mid.Length;
		string fit = "";

		// find the longest string that fits into
		// the control boundaries using bisection method
		while (seg > 1)
		{
			seg -= seg / 2;

			int left = len + seg;
			int right = mid.Length;

			[..] 

			// build and measure a candidate string with ellipsis
			string tst = mid.Substring(0, left) + 
				EllipsisChars + mid.Substring(right);

			// restore path with <drive> and <filename>
			if (isPath)
			{
				tst = Path.Combine(Path.Combine(pre, tst), post);
			}
			s = TextRenderer.MeasureText(dc, tst, ctrl.Font);

			// candidate string fits into control boundaries, 
			// try a longer string 
			// stop when seg <= 1 
			if (s.Width <= ctrl.Width)
			{
				len += seg;
				fit = tst;
			}
		}

		if (len == 0) // string can't fit into control
		{ 
			// "path" mode is off, just return ellipsis characters
			if (!isPath)
				return EllipsisChars;

			// <drive> and <directory> are empty, return <filename>
			if (pre.Length == 0 && mid.Length == 0)
				return post;

			// measure "C:\...\filename.ext"
			fit = Path.Combine(Path.Combine(pre, EllipsisChars), post);
			
			s = TextRenderer.MeasureText(dc, fit, ctrl.Font);

			// if still not fit then return "...\filename.ext"
			if (s.Width > ctrl.Width)
				fit = Path.Combine(EllipsisChars, post);
		}
		return fit;
	}
}

History

  • June 20, 2009 - Original article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberssdi17-Jul-12 23:42 
BugMemory leaks, Compact, OnChangeFont [modified] Pinmemberssdi17-Jul-12 5:48 
GeneralCan not download this file PinmemberRaymondStone5-Jul-12 17:22 
QuestionMy vote of 5 PinmemberFilip D'haene5-Sep-11 4:42 
GeneralMy vote of 5 Pinmembergunters26-May-11 0:30 
GeneralGreat code! PinmemberMember 45024086-Feb-11 22:53 
GeneralLabel with a border splits incorrectly PinmemberIvan00111-Jan-11 11:01 
GeneralRe: Label with a border splits incorrectly PinmemberThomas Polaert12-Jan-11 22:51 
QuestionHow to handle a multiline text box Pinmemberst1419-Nov-09 12:55 
GeneralThank you for this code. PinmemberTeufel121225-Aug-09 8:29 
GeneralExcellent Article / Bisection / Custom Trimming Function Pinmemberaspdotnetdev1-Jul-09 23:06 
GeneralRe: Excellent Article / Bisection / Custom Trimming Function PinmemberThomas Polaert6-Jul-09 12:54 
GeneralAwesome PinmemberDmitri Nesteruk30-Jun-09 1:08 
GeneralRe: Awesome PinmemberMember 4558668-Jun-11 2:20 
GeneralRe: Awesome PinmemberDmitri Nesteruk8-Jun-11 2:25 
GeneralVery nice Pinmemberscosta_FST26-Jun-09 3:21 
General我觉的这个主题写的还不够深入 Pinmemberzzx_12345624-Jun-09 17:08 
GeneralRe: 我觉的这个主题写的还不够深入 PinmemberRisen Dow26-Jun-09 6:14 
GeneralRe: oh my god !!!!! Pinmemberzzx_1234568-Jul-09 23:01 
GeneralGreat PinmemberAllenR21-Jun-09 23:15 
GeneralRe: Great PinmemberJoeSwan8-Oct-13 18:02 
GeneralVery nice PinmvpPete O'Hanlon21-Jun-09 12:48 
GeneralRe: Very nice PinmemberThomas Polaert24-Jun-09 6:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411019.1 | Last Updated 20 Jun 2009
Article Copyright 2009 by Thomas Polaert
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid