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

在 Silverlight TextBox 中模拟 MaxLines 属性

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011年8月18日

CPOL

3分钟阅读

viewsIcon

24994

downloadIcon

579

如何在 Silverlight TextBox 中模拟 MaxLines 属性。

引言

您肯定已经注意到 Silverlight TextBox 控件缺少臭名昭著的 MaxLines 属性,而该属性在 WPF TextBox 中非常有用,这就是您阅读本文的原因。

自从上周以来,我一直有这个需求,即限制您可以在 Silverlight 4 的多行 (AcceptsReturn=true) TextBox 中写入的行数,并且失望地发现目前没有像在 WPF 中那样做的方法。在 WPF 中,TextBox 控件有一个名为 MaxLines 的整数属性,您可以设置它来限制行数。在 Silverlight 中,似乎没有办法,因为您不可能知道在字符串中的何时何地发生了自动换行拆分。如果使用 Courier/CourierNew 等固定宽度字体,那将很简单,因为您可以简单地计算出一行可以容纳的字符数,但在我的例子中,它是 Comic Sans。客户坚持要这个功能,所以我决定编写自己的逻辑来模拟 Silverlight TextBox 中使用的自动换行算法。我别无选择,只能接受这个挑战。

使用代码

附加的代码包含一个 Visual Studio 解决方案,演示了文本框的行为。我将新控件称为 XTextBox,代表“扩展的文本框”。

第二个附件 XTextBoxControl 包含 XTextBox 控件的源代码。

您可以从此处查看 TextBox 的演示: http://briggs69.blogspot.com/2011/08/solution-maxlines-property-in.html

关注点

我所做的是扩展了 Silverlight TextBox 控件,并添加了我自己的逻辑来模拟自动换行的行为。 这是它的工作方式。

源代码中最有趣的部分是 GetNumberOfLines() 方法。 此方法返回 TextBox 中特定字符串所占用的行数。

首先,我们需要知道自动换行算法在哪些字符上将字符串拆分为“可换行”的单词,或者在本例中我称它们为标记。 通过一些实验,我发现 TextBox 自动换行算法在这些字符上拆分单词

//split string into tokens/words separated by these symbols
string tokenizers = @"(?<=[ /\\\$%\(\)\-\+\[\]\{\}\?])";

List<string> tokens = Regex.Split(strNoBreaks, tokenizers).ToList();

这里我们使用正则表达式来拆分字符串,而不是 String.Split(),这样它将包含像空格这样的拆分字符。

现在我们知道了在哪里拆分单词以模拟自动换行操作,接下来我们需要一种方法来测量字符串的实际宽度。 这样我们才能知道文本框中的特定标记组是否适合 TextBox 中的单行。 我们可以使用 TextBlock 控件来测量字符串的宽度。 因为对于 TextBlock,如果您没有显式设置宽度,则实际宽度将根据其内容自动调整。 因此,我们可以使用它们来测量字符串的实际宽度。

这是该方法的样子

protected int GetNumberOfLines(string theText = null)
{
    tbLineMeasurer.Text = "";
    string strInput = theText == null ? Text : theText;

    //split string into tokens/words separated by these symbols
    string tokenizers = @"(?<=[ /\\\$%\(\)\-\+\[\]\{\}\?])";

    List<string> tokens = Regex.Split(strInput, tokenizers).ToList();
    int tokensInNewLine = 0;

    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 the word contains a line break, add line count
        //then split the token by linebreak into multiple tokens
        //then insert each token to the list of tokens and set line count
        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++;
            }

            //Skip this token, proceed to next token.
            continue;
        }

        //append the current token to the measuring TextBlock
        tbLineMeasurer.Text += token;

        tbLineMeasurer.InvalidateMeasure();

        //if the length of the current line's string
        //plus the new token exceeds the line width,
        //increment line count, then carry
        //over the current token to the next line.
        if (tbLineMeasurer.ActualWidth >= lineWidth)
        {
            if (tbLineMeasurer.Text != token)
            //Increment line count only
            //if the token is NOT the very first token
            {
                //carry over this token to the next line
                tbLineMeasurer.Text = token;
                lineCount++; //increment line count
            }

            //If the current token's width exceeds
            //the line width, simulate token split
            //then insert the last remaining unsplitted
            //string to the list of tokens for the next iteration
            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 this current token wraps to a new line, 
                //clear the measurer then insert the token to the list
                //because this token will be in a new line
                if (tokensInNewLine <= 0)
                {
                    tokens.Insert(i + 1, tbLineMeasurer.Text);
                    tbLineMeasurer.Text = "";
                    continue;
                }
            }
        }

        //If the following tokens are part of a token
        //with linebreaks, clear the measurer
        //as if the new token is in a new line.
        if (tokensInNewLine > 0)
        {
            tbLineMeasurer.Text = string.Empty;
            tokensInNewLine--;
        }
    }

    //System.Diagnostics.Debug.WriteLine(
    //       string.Format("Line Count = {0}" ,lineCount));
    return lineCount;
}

但是,我们可以优化代码,因为您将从 TextChanged 事件调用它。 您可以添加另一个方法来粗略估计行数,然后仅在估计的行数足够接近 MaxLines 限制时才调用此方法。 对于正常情况,此方法的运行时间的最佳情况为 O(n^2),其中 n 是字符串中的标记/单词的数量,对于最坏的情况,n 是字符的数量。 所以要小心,如果你的文本框中有成千上万个单词,这可能会运行得很慢。

历史

  • 2011 年 8 月 18 日:创建了文章。
  • 2011 年 8 月 31 日:添加了指向演示的链接。 更新了算法。 修复了错误。
© . All rights reserved.