自动省略号






4.93/5 (53投票s)
为任何 Windows 窗体控件添加“自动省略号”功能

引言
当 .NET Framework 已经提供了几种内置选项来实现此任务时,为什么还要另一个省略号控件?System.Windows.Forms.Label
控件带有一个 AutoEllipsis
属性。System.Drawing.Graphics.DrawString
或 System.Windows.Forms.TextRenderer.DrawText
提供了一种可靠的方法,使文本适合预定义的边界。只需看看 StringTrimming
或 TextFormatFlags
枚举!更不用说来自 shlwapi.dll 的 PathCompactPath
API 或静态控件样式 SS_ENDELLIPSIS
和 SS_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
在获得焦点时切换到“全文”模式,以便可以像往常一样编辑其内容。当它失去焦点时,它会切换回“省略号”模式。
代码内部
找到正确的大小:二分法
一个有效的省略号算法应该找到可以适合控件边界的最长子字符串。蛮力方法将通过逐个删除字符来测试所有子字符串。提出的解决方案使用二分法来最小化迭代次数,以获得最接近的匹配。
二分法示例

该算法使用 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
方法的实际应用

使用正则表达式在单词边界处修剪
.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;
}
}
在单词边界处修剪的文本示例

修剪路径字符串
“路径”模式是一项功能,其中指定的文本被视为文件路径。该算法尽可能保留驱动器和文件名信息
- c:\directory1\dir...\filename.ext
- c:\...\filename.ext
- ...\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 日 - 原始文章