在 WPF 中组合文本环绕和收缩

Combining text wrapping and shrinking in WPF

我的 WPF 应用程序有一个相对较小的宽度有限的框,我需要在其中显示用户输入的一些文本。文本实际上可以预期在 1 到 5 个单词之间,但单词很容易比框大。

如果文本太长,但包含多个可以分成几行的单词,我希望文本换行。但是,如果任何单个单词太大而不适合,那么我希望缩小文本大小直到该单词足够小以适合,无论该文本是否也在换行。我不在乎文本占据多少垂直 space。

这是我在 Excel 中手动组合的示例,用于演示预期的行为:

在示例 1 中,整个文本都适合方框。
示例2中的文本是两个单词,因此可以在不缩小文本的情况下换行。
例3中,单个单词太长,需要缩小文字。
在示例 4 中,文本可以换行,但它仍然包含一个太长的单词,因此文本必须缩小,直到最长的单词可以容纳。

如何在 WPF 中完成此操作?我一直没能找到 ViewBoxTextBlock.TextWrapping 的组合。

编辑:
如果我 do 必须手动执行此操作(这有点像噩梦),那么至少有一种方法可以弄清楚 TextBlock 决定的是 "line"?我需要知道它将如何分解文本,然后才能确定 "line" 是否会太长。

您必须手动执行此操作。以下代码示例调整 TextBox 的字体大小,直到所有文本都适合视口(最大可用 space 用于文本呈现)。您必须从注册到 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 扩展您自己的 class 来封装此行为。

此示例依赖于具有固定 WidthHeightTextBox,因此它无法根据内容调整大小。

由于没有真正的解决方案,我最终自己编写了代码:

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

此 class 使用附加的依赖属性将我需要的行为实现为 WPF 附加行为。魔术发生在最后的例程中:Update.

在 WPF 中,如果给定元素被剪裁(即它比 space 允许占用的元素大,因此它被截断),那么 LayoutInformation.GetLayoutClip return s 关于元素的哪个区域可见的数据。如果一个元素没有被剪裁,这似乎 return null(尽管文档没有这么说)。
TextWrapping="WrapWithOverflow" 的 TextBlock 将 "overflow" 超过其容器的边缘,如果任何一行太大而无法正确断开。
Update 例程检查是否正在发生此裁剪,如果发生,则将字体大小减小 1。这会更改 TextBlock 的大小并触发另一轮 Update,继续循环直到元素不再剪辑。
如果可用 space 增加,还有其他逻辑可以将字体缩放回其原始大小。

一个用法示例是:

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

请记住 TextWrapping="WrapWithOverflow" 是必需的。