Как настроить элемент управления TextBox для автоматического изменения размера по вертикали, когда текст больше не подходит для одной строки?

Как настроить элемент управления TextBox для автоматического изменения размера по вертикали, когда текст больше не подходит для одной строки?

Например, в следующем XAML:

<DockPanel LastChildFill="True" Margin="0,0,0,0">
  <Border Name="dataGridHeader" 
    DataContext="{Binding Descriptor.Filter}"
    DockPanel.Dock="Top"                         
    BorderThickness="1"
    Style="{StaticResource ChamelionBorder}">
  <Border
    Padding="5"
    BorderThickness="1,1,0,0"                            
    BorderBrush="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=dc:NavigationPane, 
    ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleBorder}}}">
    <StackPanel Orientation="Horizontal">
      <TextBlock                                
        Name="DataGridTitle"                                                                                                
        FontSize="14"
        FontWeight="Bold"                                    
        Foreground="{DynamicResource {ComponentResourceKey 
        TypeInTargetAssembly=dc:NavigationPane, 
        ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}"/>
      <StackPanel Margin="5,0"  Orientation="Horizontal" 
              Visibility="{Binding IsFilterEnabled, FallbackValue=Collapsed, Mode=OneWay, Converter={StaticResource BooleanToVisibility}}"
              IsEnabled="{Binding IsFilterEnabled, FallbackValue=false}"  >                                    
          <TextBlock  />                                                                
          <TextBox    
            Name="VerticallyExpandMe"
            Padding="0, 0, 0, 0"  
            Margin="10,2,10,-1"                                                                                                                                                                                                                     
            AcceptsReturn="True"
            VerticalAlignment="Center"                                    
            Text="{Binding QueryString}"
            Foreground="{DynamicResource {ComponentResourceKey 
            TypeInTargetAssembly=dc:NavigationPane, 
            ResourceId={x:Static dc:NavigationPaneColors.NavPaneTitleForeground}}}">
          </TextBox>
        </StackPanel>                               
    </StackPanel>
  </Border>              
  </Border>
</DockPanel>

Элемент управления TextBox с именем "VerticalExpandMe" должен автоматически расширяться по вертикали, когда привязанный к нему текст не подходит для одной строки. Если для параметра AcceptsReturn установлено значение true, TextBox расширяется вертикально, если я нажимаю кнопку ввода внутри него, но я хочу, чтобы он делал это автоматически.

Ответ 1

Хотя предложение Андре Лууса в основном правильное, на самом деле он не будет работать здесь, потому что ваш макет будет прервать перенос текста. Я объясню, почему.

В основном проблема заключается в следующем: текстовая упаковка только делает что-либо, когда ширина элемента ограничена, но ваш TextBox имеет неограниченную ширину, потому что это потомок горизонтального StackPanel. (Ну, две горизонтальные панели стека. Возможно, больше, в зависимости от контекста, из которого вы взяли ваш пример.) Поскольку ширина не ограничена, TextBox не имеет представления, когда он должен начать обертывание, и поэтому он никогда не будет завернут, даже если вы включите упаковку. Вам нужно сделать две вещи: ограничить ее ширину и включить обертку.

Вот более подробное объяснение.

В вашем примере содержится много деталей, не имеющих отношения к проблеме. Здесь версия, которую я немного подрезал, чтобы было легче объяснить, что не так:

<StackPanel Orientation="Horizontal">
    <TextBlock Name="DataGridTitle" />
    <StackPanel
        Margin="5,0"
        Orientation="Horizontal"
        >
        <TextBlock />
        <TextBox
            Name="VerticallyExpandMe"
            Margin="10,2,10,-1"
            AcceptsReturn="True"
            VerticalAlignment="Center"
            Text="{Binding QueryString}"
            >
        </TextBox>
    </StackPanel>
</StackPanel>

Итак, я удалил ваш содержащий DockPanel и два вложенных элемента Border внутри этого, потому что они не являются частью проблемы и не имеют отношения к решению. Поэтому я начинаю с пары вложенных элементов StackPanel в вашем примере. И я также удалил большинство атрибутов, потому что большинство из них также не имеют отношения к макету.

Это выглядит немного странно: наличие двух вложенных горизонтальных стековых панелей, подобных этому, выглядит излишним, но на самом деле это имеет смысл в вашем оригинале, если вам нужно сделать вложенный один видимым или невидимым во время выполнения. Но это облегчает решение проблемы.

(Пустое тег TextBlock также странно, но точно так же, как оно появляется в вашем оригинале. Это, кажется, ничего полезного не делает.)

И вот проблема: ваш TextBox находится внутри некоторых горизонтальных элементов StackPanel, что означает, что его ширина не ограничена - вы случайно объявили текстовое поле, что оно может свободно расти до любой ширины, независимо от того, сколько места фактически доступны.

A StackPanel всегда будет выполнять макет, который не ограничен в направлении укладки. Поэтому, когда дело доходит до того, что TextBox, оно будет проходить в горизонтальном размере double.PositiveInfinity до TextBox. Поэтому TextBox всегда будет думать, что у него больше места, чем нужно. Более того, когда дочерний элемент StackPanel запрашивает больше места, чем фактически доступен, StackPanel лежит и делает вид, что дает ему столько места, но затем посещает его.

(Это цена, которую вы платите за предельную простоту StackPanel - она ​​проста в том, чтобы быть костяной, потому что она с радостью построит макеты, которые на самом деле не подходят. Вы должны использовать только StackPanel если у вас действительно есть неограниченное пространство, потому что вы находитесь внутри ScrollViewer, или вы уверены, что у вас достаточно мало элементов, из которых вы не закончите свободное пространство, или если вам не нужны элементы, запущенные с конца панели, когда они становятся слишком большими, и вы не хотите, чтобы система макета пыталась сделать что-нибудь более умное, чем просто обрезать контент.)

Поэтому включение текстовой оболочки не поможет здесь, потому что StackPanel всегда будет притворяться, что для текста больше места для текста.

Вам нужна другая структура макета. Панели стоек - это неправильная вещь, потому что они не будут налагать ограничение макета, которое вам нужно, чтобы получить перенос текста.

Вот простой пример, который делает примерно то, что вы хотите:

<Grid VerticalAlignment="Top">
    <DockPanel>
        <TextBlock
            x:Name="DataGridTitle"
            VerticalAlignment="Top"
            DockPanel.Dock="Left"
            />
        <TextBox
            Name="VerticallyExpandMe"
            AcceptsReturn="True"
            TextWrapping="Wrap"
            Text="{Binding QueryString}"
            >
        </TextBox>
    </DockPanel>
</Grid>

Если вы создаете новое приложение WPF и вставляете его в качестве содержимого главного окна, вы должны найти, что он делает то, что вы хотите - TextBox начинается с одной строки, заполняет доступную ширину, и если вы введите текст в, он будет расти по одной строке за раз, когда вы добавите больше текста.

Конечно, поведение макета всегда чувствительно к контексту, поэтому его может быть недостаточно, чтобы просто выбросить его в середину существующего приложения. Это будет работать, если вставить в пространство фиксированного размера (например, как тело окна), но не будет работать корректно, если вы вставьте его в контекст, где ширина не ограничена. (Например, внутри a ScrollViewer или внутри горизонтали StackPanel.)

Итак, если это не сработает для вас, это будет из-за других вещей, которые не так в другом месте вашего макета - возможно, еще больше элементов StackPanel в другом месте. Из внешнего вида вашего примера, вероятно, стоит потратить некоторое время на размышления о том, что вам действительно нужно в вашем макете и его упрощении, - наличие отрицательных полей и элементы, которые, похоже, ничего не делают, такие пустые TextBlock обычно что указывает на чрезмерно сложную компоновку. И излишняя сложность в макете значительно усложняет достижение желаемых результатов.

Ответ 2

В качестве альтернативы вы можете ограничить TextBlock Width привязкой к родительскому ActualWidth, например:

<TextBlock Width="{Binding ElementName=*ParentElement*, Path=ActualWidth}" 
           Height="Auto" />

Это заставит его автоматически изменять размер своей высоты.

Ответ 3

Используйте MaxWidth и TextWrapping="WrapWithOverflow".

Ответ 4

Я использую другой простой подход, который позволяет мне не менять макет документа.

Основная идея - не устанавливать элемент управления Width, прежде чем он начнет меняться. Для TextBox es я обрабатываю событие SizeChanged:

<TextBox TextWrapping="Wrap" SizeChanged="TextBox_SizeChanged" />

private void TextBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
    FrameworkElement box = (FrameworkElement)sender;
    if (e.PreviousSize.Width == 0 || box.Width < e.PreviousSize.Width)
        return;
    box.Width = e.PreviousSize.Width;
}

Ответ 5

Вы можете использовать этот класс, который расширяет TextBlock. Он автоматически сжимается и учитывает MaxHeight/MaxWidth:

public class TextBlockAutoShrink : TextBlock
    {
        private double _defaultMargin = 6;
        private Typeface _typeface;

        static TextBlockAutoShrink()
        {
            TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
        }

        public TextBlockAutoShrink() : base() 
        {
            _typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
            base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
        }

        private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var t = sender as TextBlockAutoShrink;
            if (t != null)
            {
                t.FitSize();
            }
        }

        void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            FitSize();
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            FitSize();

            base.OnRenderSizeChanged(sizeInfo);
        }


        private void FitSize()
        {
            FrameworkElement parent = this.Parent as FrameworkElement;
            if (parent != null)
            {
                var targetWidthSize = this.FontSize;
                var targetHeightSize = this.FontSize;

                var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
                var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;

                if (this.ActualWidth > maxWidth)
                {
                    targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
                }

                if (this.ActualHeight > maxHeight)
                {
                    var ratio = maxHeight / (this.ActualHeight);

                    // Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
                    // And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
                    ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;

                    targetHeightSize = (double)(this.FontSize *  ratio);
                }

                this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
            }
        }
    }