Сочетание переноса текста и сжатия в WPF

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

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

Вот пример, который я собрал вручную в Excel, чтобы продемонстрировать предполагаемое поведение: Пример

В примере 1 весь текст умещается в рамке.
В примере 2 текст состоит из двух слов, поэтому его можно перенести без сжатия текста.
В примере 3 одно слово слишком длинное, поэтому текст быть сжатым.
В примере 4 текст может быть перенесен, но он все еще содержит слово, которое слишком длинное, поэтому текст должен быть сжат до тех пор, пока это самое длинное слово не уместится.

Как я могу сделать это в WPF? Я не смог найти комбинацию ViewBox и TextBlock.TextWrapping, которая делает это.

EDIT:
Если мне сделать нужно сделать это вручную (что было бы немного кошмаром), то есть ли способ, по крайней мере, я могу выяснить, что TextBlock решает, что это «строка»? Мне нужно было бы знать, как он будет разбивать текст, прежде чем я смогу определить, будет ли какая-либо «строка» слишком длинной.


person Keith Stein    schedule 10.07.2019    source источник


Ответы (2)


Вам следует сделать это вручную. В следующем примере кода размер шрифта TextBox настраивается до тех пор, пока весь текст не уместится в области просмотра (максимально доступное пространство для рендеринга текста). Вы должны выполнить этот метод из обработчика событий, который зарегистрирован для события TextBoxBase.TextChanged:

protected void ResizeTextToFit(TextBox textBox)
{
  // Make sure the first line is always visible
  textBox.ScrollToVerticalOffset(0);

  bool fontSizeHasChanged = false;

  // Shrink to fit as long
  // the last visible line is not the last line or
  // the true text height is bigger than the visible text height
  // and prevent font size to be set to '0'
  while (textBox.FontSize > 1 
         && (textBox.GetLastVisibleLineIndex() < textBox.LineCount - 1 
             || textBox.ExtentHeight > textBox.ViewportHeight))
  {
    fontSizeHasChanged = true;
    textBox.FontSize -= 1.0;
  }

  if (fontSizeHasChanged)
  {
    return;
  }

  // Enlarge to fit as long the last line is visible 
  // and the text height fits into the viewport
  while (textBox.GetLastVisibleLineIndex() == textBox.LineCount - 1 
         && textBox.ExtentHeight < textBox.ViewportHeight)
  {
    textBox.FontSize += 1.0;
  }
  textBox.FontSize -= 1.0;
}

Вы можете предпочесть расширить свой собственный класс от TextBox, чтобы инкапсулировать это поведение.

Этот пример зависит от TextBox, который имеет фиксированные Width и Height, поэтому он не может изменять размер содержимого.

person BionicCode    schedule 11.07.2019

Видя, что реального решения для этого не существует, я сам написал его:

Imports System.ComponentModel
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Documents

    Public Class TextScalerBehavior
        Public Shared ReadOnly ShrinkToFitProperty As DependencyProperty = DependencyProperty.RegisterAttached("ShrinkToFit", GetType(Boolean), GetType(TextScalerBehavior), New PropertyMetadata(False, New PropertyChangedCallback(AddressOf ShrinkToFitChanged)))

        Public Shared Function GetShrinkToFit(obj As TextBlock) As Boolean
            Return obj.GetValue(ShrinkToFitProperty)
        End Function

        Public Shared Sub SetShrinkToFit(obj As TextBlock, value As Boolean)
            obj.SetValue(ShrinkToFitProperty, value)
        End Sub

        Protected Shared Sub ShrinkToFitChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
            Dim tb As TextBlock = d

            If e.NewValue Then
                tb.AddHandler(TextBlock.SizeChangedEvent, TargetSizeChangedEventHandler)
                With DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, GetType(TextBlock))
                    .AddValueChanged(tb, TargetTextChangedEventHandler)
                End With
                tb.AddHandler(TextBlock.LoadedEvent, TargetLoadedEventHandler)
            Else
                tb.RemoveHandler(TextBlock.SizeChangedEvent, TargetSizeChangedEventHandler)
                With DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, GetType(TextBlock))
                    .RemoveValueChanged(tb, TargetTextChangedEventHandler)
                End With
                tb.RemoveHandler(TextBlock.LoadedEvent, TargetLoadedEventHandler)
            End If
        End Sub

        Protected Shared ReadOnly TargetSizeChangedEventHandler As New RoutedEventHandler(AddressOf TargetSizeChanged)

        Protected Shared Sub TargetSizeChanged(Target As TextBlock, e As RoutedEventArgs)
            Update(Target)
        End Sub

        Protected Shared ReadOnly TargetTextChangedEventHandler As New EventHandler(AddressOf TargetTextChanged)

        Protected Shared Sub TargetTextChanged(Target As TextBlock, e As EventArgs)
            Update(Target)
        End Sub

        Protected Shared ReadOnly TargetLoadedEventHandler As New RoutedEventHandler(AddressOf TargetLoaded)

        Protected Shared Sub TargetLoaded(Target As TextBlock, e As RoutedEventArgs)
            Update(Target)
        End Sub

        Private Shared ReadOnly Shrinkging As New HashSet(Of TextBlock)

        Protected Shared Sub Update(Target As TextBlock)
            If Target.IsLoaded Then
                Dim Clip = Primitives.LayoutInformation.GetLayoutClip(Target)

                If Clip IsNot Nothing Then
                    If Not Shrinkging.Contains(Target) Then Shrinkging.Add(Target)
                    Target.FontSize -= 1
                ElseIf Target.FontSize < TextElement.GetFontSize(Target.Parent) Then
                    If Shrinkging.Contains(Target) Then
                        Shrinkging.Remove(Target)
                    Else
                        Target.FontSize += 1
                    End If
                End If
            End If
        End Sub
    End Class

Этот класс реализует нужное мне поведение как присоединенное поведение WPF с использованием прикрепленных свойств зависимостей. Волшебство происходит в финальной программе: Update.

В WPF, если данный элемент обрезается (то есть он больше, чем пространство, которое ему разрешено занимать, поэтому он обрезается), тогда LayoutInformation.GetLayoutClip возвращает данные о том, какая область элемента видна. Если элемент не обрезан, кажется, что он возвращает значение null (хотя в документации об этом не говорится).
TextBlock с TextWrapping="WrapWithOverflow" будет "переполняться" за края своего контейнера, если какая-либо отдельная строка слишком велика, чтобы ее можно было разбить. правильно.
Подпрограмма Update проверяет, происходит ли это отсечение, и если да, то уменьшает размер шрифта на 1. Это изменяет размер TextBlock и запускает еще один раунд Update, который продолжает цикл до тех пор, пока элемент больше не обрезается. .
Существует также дополнительная логика для масштабирования шрифта до исходного размера, если доступное пространство увеличивается.

Пример использования:

<TextBlock [YourNamespace]:TextScalerBehavior.ShrinkToFit="True" TextWrapping="WrapWithOverflow"/>

Помните, что TextWrapping="WrapWithOverflow" требуется.

person Keith Stein    schedule 30.07.2019