Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / Windows Forms
Article

Auto Ellipsis

Rate me:
Please Sign up or sign in to vote.
4.93/5 (55 votes)
20 Jun 2009CPOL3 min read 473.6K   1.9K   113   27
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:

C#
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:

C#
[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):

C#
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.

C#
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).
C#
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)


Written By
France France
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to handle a multiline text box Pin
st1419-Nov-09 11:55
st1419-Nov-09 11:55 

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

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