在Silverlight中,TextBox控件缺少一个在WPF中非常有用的属性——MaxLines。这个属性允许开发者限制用户在TextBox中输入的行数。由于Silverlight的TextBox没有这个属性,开发者需要自己实现类似的功能。本文将介绍如何在Silverlight 4中实现这一功能。
自从上周接到一个需求,需要在Silverlight4中的多行TextBox(AcceptsReturn=true)中限制用户输入的行数,就开始寻找解决方案。遗憾的是,Silverlight并没有提供这样的属性。在WPF中,TextBox控件有一个名为MaxLines的整数属性,可以设置行数限制。但在Silverlight中,由于无法确定字符串在何处进行了换行,因此实现起来比较困难。
为了满足客户的需求,决定自己编写逻辑来模拟SilverlightTextBox中的自动换行算法。创建了一个名为XTextBox的自定义控件,它扩展了Silverlight的TextBox控件,并添加了自己的逻辑来模拟换行行为。
最有趣的部分是GetNumberOfLines()方法,它返回某个字符串在TextBox中占用的行数。首先,需要知道在哪些字符处,换行算法将字符串分割成可以换行的单词,或者在这种情况下,称之为标记(tokens)。
通过一些实验,发现TextBox的换行算法在这些字符处分割单词:
string tokenizers = @"
(?<=[ /\\\$%\(\)\-\+\[\]\{\}\?])";
使用正则表达式来分割字符串,而不是String.Split(),这样它将包括分割字符,如空格。
现在知道了如何分割单词以模拟换行操作,接下来需要一种方法来测量字符串的实际宽度。这样就可以知道TextBox中的某个标记组是否适合单行。可以使用TextBlock控件来测量字符串的宽度。因为如果不显式设置TextBlock的宽度,其实际宽度将根据其内容自动调整。所以,可以使用它们来测量字符串的实际宽度。
以下是该方法的实现:
protected int GetNumberOfLines(string theText = null)
{
tbLineMeasurer.Text = "";
string strInput = theText == null ? Text : theText;
List tokens = Regex.Split(strInput, tokenizers).ToList();
int tokensInNewLine = 0;
int lineCount = 1;
for (var i = 0; i < tokens.Count; i++)
{
string token = tokens[i];
if (token == string.Empty)
{
if (tokensInNewLine > 0)
{
tbLineMeasurer.Text = string.Empty;
tokensInNewLine--;
}
continue;
}
if (token.Contains("\r"))
{
string[] s = token.Split('\r');
lineCount += s.Count() - 1;
tokensInNewLine = s.Count() - 1;
int j = i + 1;
foreach (var tok in s)
{
tokens.Insert(j, tok);
j++;
}
continue;
}
tbLineMeasurer.Text += token;
tbLineMeasurer.InvalidateMeasure();
if (tbLineMeasurer.ActualWidth >= lineWidth)
{
if (tbLineMeasurer.Text != token)
{
tbLineMeasurer.Text = token;
lineCount++;
}
if (tbLineMeasurer.ActualWidth >= lineWidth)
{
char[] arrToken = tbLineMeasurer.Text.ToArray();
tbLineMeasurer.Text = "";
foreach (var ch in arrToken)
{
tbLineMeasurer.Text += ch;
if (tbLineMeasurer.ActualWidth > lineWidth)
{
lineCount++;
tbLineMeasurer.Text = new String(ch, 1);
}
}
if (tokensInNewLine <= 0)
{
tokens.Insert(i + 1, tbLineMeasurer.Text);
tbLineMeasurer.Text = "";
continue;
}
}
}
if (tokensInNewLine > 0)
{
tbLineMeasurer.Text = string.Empty;
tokensInNewLine--;
}
}
return lineCount;
}
可以优化这段代码,因为将从TextChanged事件中调用它。可以添加另一个方法来大致估计行数,然后只有在估计的行数足够接近MaxLines限制时才调用这个方法。
这个方法在最坏情况下的运行时间是O(n^2),其中n是字符串中的标记/单词数量,对于正常情况,n是字符数量。所以要小心,如果TextBox中有成千上万的单词,它可能会运行得很慢。