Visible line count of a TextBlock
Asked Answered
M

4

10

If you set TextWrapping to "Wrap", a WPF TextBlock can have several lines of text. Is there a "clean" way to get the number of lines of text? I considered looking at the desired height and dividing it by an estimated height of each line. However, that seems quite dirty. Is there a better way?

Marasmus answered 9/7, 2009 at 19:27 Comment(0)
D
9

One thing about WPF that's very nice is that all of the controls are very lookless. Because of this, we can make use of TextBox, which has a LineCount property (Why it's not a DependencyProperty or why TextBlock doesn't also have it I do not know). With the TextBox, we can simply re-template it so it behaves and looks more like a TextBlock. In our custom Style/Template we're going to set IsEnabled to False, and just create a basic re-templating of the control so that the disabled look is no longer present. We can also bind any properties we want to maintain, like Background, through the use of TemplateBindings.

<Style x:Key="Local_TextBox"
    TargetType="{x:Type TextBoxBase}">
    <Setter Property="IsEnabled"
            Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">
                <Border Name="Border"
                    Background="{TemplateBinding Background}">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
</Setter>
</Style>

Now, that will take care of making our TextBox look and behave like a TextBlock, but how do we get the line count?

Well, if we want to access it directly in the code behind then we can register to the TextBox's SizeChanged Event.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        LongText = "This is a long line that has lots of text in it.  Because it is a long line, if a TextBlock's TextWrapping property is set to wrap then the text will wrap onto new lines. However, we can also use wrapping on a TextBox, that has some diffrent properties availible and then re-template it to look just like a TextBlock!";

        uiTextBox.SizeChanged += new SizeChangedEventHandler(uiTextBox_SizeChanged);

        this.DataContext = this;
    }

    void uiTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        Lines = uiTextBox.LineCount;
    }

    public string LongText { get; set; }

    public int Lines
    {
        get { return (int)GetValue(LinesProperty); }
        set { SetValue(LinesProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Lines.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty LinesProperty =
        DependencyProperty.Register("Lines", typeof(int), typeof(MainWindow), new UIPropertyMetadata(-1));
}

However, since I tend to need to use properties like that in places other then the current window, and/or am using MVVM and don't want to take that approach, then we can create some AttachedProperties to handle the retrieval and setting of the LineCount. We're going to use the AttachedProperties to do the same thing, but now we'll be able to use it with any TextBox anywhere, and bind to it through that TextBox instead of the Window's DataContext.

public class AttachedProperties
{
    #region BindableLineCount AttachedProperty
    public static int GetBindableLineCount(DependencyObject obj)
    {
        return (int)obj.GetValue(BindableLineCountProperty);
    }

    public static void SetBindableLineCount(DependencyObject obj, int value)
    {
        obj.SetValue(BindableLineCountProperty, value);
    }

    // Using a DependencyProperty as the backing store for BindableLineCount.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty BindableLineCountProperty =
        DependencyProperty.RegisterAttached(
        "BindableLineCount",
        typeof(int),
        typeof(MainWindow),
        new UIPropertyMetadata(-1));

    #endregion // BindableLineCount AttachedProperty

    #region HasBindableLineCount AttachedProperty
    public static bool GetHasBindableLineCount(DependencyObject obj)
    {
        return (bool)obj.GetValue(HasBindableLineCountProperty);
    }

    public static void SetHasBindableLineCount(DependencyObject obj, bool value)
    {
        obj.SetValue(HasBindableLineCountProperty, value);
    }

    // Using a DependencyProperty as the backing store for HasBindableLineCount.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HasBindableLineCountProperty =
        DependencyProperty.RegisterAttached(
        "HasBindableLineCount",
        typeof(bool),
        typeof(MainWindow),
        new UIPropertyMetadata(
            false,
            new PropertyChangedCallback(OnHasBindableLineCountChanged)));

    private static void OnHasBindableLineCountChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var textBox = (TextBox)o;
        if ((e.NewValue as bool?) == true)
        {
            textBox.SetValue(BindableLineCountProperty, textBox.LineCount);
            textBox.SizeChanged += new SizeChangedEventHandler(box_SizeChanged);
        }
        else
        {
            textBox.SizeChanged -= new SizeChangedEventHandler(box_SizeChanged);
        }
    }

    static void box_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        var textBox = (TextBox)sender;
        (textBox).SetValue(BindableLineCountProperty, (textBox).LineCount);
    }
    #endregion // HasBindableLineCount AttachedProperty
}

Now, it's simple to find the LineCount:

<StackPanel>
    <TextBox x:Name="uiTextBox"
             TextWrapping="Wrap"
             local:AttachedProperties.HasBindableLineCount="True"
             Text="{Binding LongText}"
             Style="{StaticResource Local_TextBox}" />

    <TextBlock Text="{Binding Lines, StringFormat=Binding through the code behind: {0}}" />
    <TextBlock Text="{Binding ElementName=uiTextBox, Path=(local:AttachedProperties.BindableLineCount), StringFormat=Binding through AttachedProperties: {0}}" />
</StackPanel>
Defeatist answered 9/7, 2009 at 21:0 Comment(1)
This is great. However, TextBox are more limited than TextBlock because they have a uniform Font family/ font size. As a result, it's easy to compute the line count. TextBlocks on the other hand can have different inlines with different heights. This makes things somewhat more difficult.Marasmus
B
3
// this seems to do the job        

<TextBox x:Name="DescriptionTextBox"
                         Grid.Row="03"
                         Grid.RowSpan="3"
                         Grid.Column="01"
                         Width="100"
                         AcceptsReturn="True"
                         MaxLength="100"
                         MaxLines="3"
                         PreviewKeyDown="DescriptionTextBox_PreviewKeyDown"
                         Text="{Binding Path=Description,
                                        Mode=TwoWay,
                                        UpdateSourceTrigger=PropertyChanged}"
                         TextWrapping="Wrap" />



        /// <summary>
        /// we need to limit a multi line textbox at entry time
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void DescriptionTextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            TextBox thisTextBox = sender as TextBox;
            if (thisTextBox != null)
            {
                // only check if we have passed the MaxLines 
                if (thisTextBox.LineCount > thisTextBox.MaxLines)
                {
                    // we are going to discard the last entered character
                    int numChars = thisTextBox.Text.Length;

                    // force the issue
                    thisTextBox.Text = thisTextBox.Text.Substring(0, numChars - 1);

                    // set the cursor back to the last allowable character
                    thisTextBox.SelectionStart = numChars - 1;

                    // disallow the key being passed in
                    e.Handled = true;
                }
            }
        }
Barneybarnhart answered 20/4, 2012 at 1:6 Comment(1)
The question was about a TextBlock not TextBox.Leaved
S
2

I have seen that this question is already 7 years old, but I just came with a solution:

The TextBlock have a private property called LineCount. I created an extension method to read this value:

public static class TextBlockExtension
{
    public static int GetLineCount(this TextBlock tb)
    {
        var propertyInfo = GetPrivatePropertyInfo(typeof(TextBlock), "LineCount");
        var result = (int)propertyInfo.GetValue(tb);
        return result;
    }

    private static PropertyInfo GetPrivatePropertyInfo(Type type, string propertyName)
    {
        var props = type.GetProperties(BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.NonPublic);
        return props.FirstOrDefault(propInfo => propInfo.Name == propertyName);
    }
}
Synonymize answered 8/2, 2017 at 10:27 Comment(0)
T
-3

The simple way is the LineCount property. Also you have a method called GetLastVisibleLineIndex that let you know how many lines the textbox can display (without scroll bars).

If you want to know when a line is added you can hear at the TextChanged event and ask about the LineCount property (you will need to keep the las LineCount into a variable to do the comparation).

Triplane answered 22/12, 2009 at 13:48 Comment(1)
TextBlock does not have a LineCount property. That's solely the domain of TextBox.Tetrasyllable

© 2022 - 2024 — McMap. All rights reserved.