Click here to Skip to main content
6,822,613 members and growing! (19,699 online)
Email Password   helpLost your password?
Languages » C# » Windows Forms     Intermediate License: The Code Project Open License (CPOL)

Auto Ellipsis

By Thomas Polaert

Add "Auto Ellipsis" feature to any Windows Form control
C# (C#2.0), Windows, .NET (.NET2.0), WinForms, Dev
Posted:20 Jun 2009
Views:9,535
Bookmarked:78 times
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
40 votes for this article.
Popularity: 7.63 Rating: 4.76 out of 5

1

2
2 votes, 5.0%
3
4 votes, 10.0%
4
34 votes, 85.0%
5
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)

About the Author

Thomas Polaert


Member

Location: France France

Other popular C# articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 12 of 12 (Total in Forum: 12) (Refresh)FirstPrevNext
GeneralHow to handle a multiline text box Pinmemberst1412:55 19 Nov '09  
GeneralThank you for this code. PinmemberTeufel12128:29 25 Aug '09  
GeneralExcellent Article / Bisection / Custom Trimming Function Pinmemberaspdotnetdev23:06 1 Jul '09  
GeneralRe: Excellent Article / Bisection / Custom Trimming Function PinmemberThomas Polaert12:54 6 Jul '09  
GeneralAwesome PinmemberDmitri Nesteruk1:08 30 Jun '09  
GeneralVery nice Pinmemberscosta_FST3:21 26 Jun '09  
General我觉的这个主题写的还不够深入 Pinmemberzzx_12345617:08 24 Jun '09  
GeneralRe: 我觉的这个主题写的还不够深入 PinmemberRisen Dow6:14 26 Jun '09  
GeneralRe: oh my god !!!!! Pinmemberzzx_12345623:01 8 Jul '09  
GeneralGreat PinmemberAllenR23:15 21 Jun '09  
GeneralVery nice PinmvpPete O'Hanlon12:48 21 Jun '09  
GeneralRe: Very nice PinmemberThomas Polaert6:39 24 Jun '09  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

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

PermaLink | Privacy | Terms of Use
Last Updated: 20 Jun 2009
Editor: Deeksha Shenoy
Copyright 2009 by Thomas Polaert
Everything else Copyright © CodeProject, 1999-2010
Web11 | Advertise on the Code Project