65.9K
CodeProject 正在变化。 阅读更多。
Home

自动省略号

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (53投票s)

2009年6月20日

CPOL

3分钟阅读

viewsIcon

481116

downloadIcon

1986

为任何 Windows 窗体控件添加“自动省略号”功能

demo

引言

当 .NET Framework 已经提供了几种内置选项来实现此任务时,为什么还要另一个省略号控件?System.Windows.Forms.Label 控件带有一个 AutoEllipsis 属性。System.Drawing.Graphics.DrawStringSystem.Windows.Forms.TextRenderer.DrawText 提供了一种可靠的方法,使文本适合预定义的边界。只需看看 StringTrimmingTextFormatFlags 枚举!更不用说来自 shlwapi.dllPathCompactPath API 或静态控件样式 SS_ENDELLIPSISSS_PATHELLIPSIS

不幸的是,内置的自动省略号控件在省略号对齐方面根本不提供任何灵活性。文本总是从 字符串的末尾截断。这可能是一个问题,例如在下面的例子中

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

内置的自动省略号控件按如下方式显示路径

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

如果保留路径的最后一部分,那将会很有帮助,因为在这种情况下它更重要。

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

顺便说一句,Visual Studio 2005 在“文件/最近使用的文件”菜单中的行为就是这样。

Using the Code

这就是我提出 Ellipsis 类的原因。它是一个 static 类,只有一个方法

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

Compact 函数截断参数 text 以使其适合 ctrl 边界。 EllipsisFormat 枚举定义如下

[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
}

Ellipsis 类可用于在各种 Windows Form 控件上实现灵活的自动省略号。我在演示项目中提供了两个示例,一个用于 Label 控件,一个用于 TextBox 控件。

TextBoxEllipsis

TextBoxEllipsis 在获得焦点时切换到“全文”模式,以便可以像往常一样编辑其内容。当它失去焦点时,它会切换回“省略号”模式。

代码内部

找到正确的大小:二分法

一个有效的省略号算法应该找到可以适合控件边界的最长子字符串。蛮力方法将通过逐个删除字符来测试所有子字符串。提出的解决方案使用二分法来最小化迭代次数,以获得最接近的匹配。

二分法示例

Bisection method

该算法使用 TextRenderer.MeasureText 方法来获取在指定控件上绘制的指定文本的大小(以像素为单位)(使用控件的字体)。二分法实现如下(为了清晰起见,删除了一些代码)

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;
	}
}

Compact 方法的实际应用

Ellipsis algorithm

使用正则表达式在单词边界处修剪

.NET Framework 允许在单词边界处修剪文本。我们通过使用正则表达式调整子字符串的边界来实现它

  • "\w*\W*" 匹配一个单词后跟空格
  • "\W*\w*$" 匹配空格后跟一个单词在字符串的结尾

从子字符串中减去这些匹配项(根据省略号对齐方式),以便在单词边界处舍入文本。

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;
	}
}

在单词边界处修剪的文本示例

Trim at a word boundary

修剪路径字符串

“路径”模式是一项功能,其中指定的文本被视为文件路径。该算法尽可能保留驱动器和文件名信息

  1. c:\directory1\dir...\filename.ext
  2. c:\...\filename.ext
  3. ...\filename.ext(这是可能的最短路径,文件名和扩展名不会被截断)。
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;
	}
}

历史

  • 2009 年 6 月 20 日 - 原始文章
© . All rights reserved.