How do I combine word wrap and dynamic font size in an WPF ItemTemplate

The recommendations I see for the individual items are to use a TextBlock with TextWrapping="true" for for former, and a Viewbox for the latter. However the two don't play nicely together. The only one I've seen for combining the two was to explicitly set a [Width on the TextBlock][1], but that requires knowing the width of the text in advanced because different lengths of text only work out nicely with different widths making in unsuitable for use with templating because the ideal length will be variable and not known in advance.

Without setting an explicit width what I get is:

enter image description here

Which works OK for the two single word items, but the multi-word one would fill the area much better if wrapped over multiple lines.

Setting Width="80" on the TextBlock gets the multi-word text to wrap nicely; but screws up the layout of single word items.

enter image description here

What I'd like is something that scales single word elements to fit (like the first two buttons in the top image) and wraps multiple word items prior to scaling to make better use of the available space (similar to the third button in the second example - although there's no need to limit it to only two rows of text if 3 or more would work better).

My XAML used for the examples above is:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="525">
    <Window.Resources>
        <local:MYViewModel x:Key="myVM"/>
    </Window.Resources>
    <Grid  DataContext="{Binding Source={StaticResource myVM}}">
        <ItemsControl ItemsSource="{Binding ThingsList, Mode= OneWay}"
                      HorizontalAlignment="Stretch" >
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="3" Rows="1"
                                 HorizontalAlignment="Stretch" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button>
                        <Viewbox>
                            <TextBlock TextWrapping="Wrap" Text="{Binding Name}" />
                        </Viewbox>
                    </Button>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

And behind it:

public class NamedThing
{
    public string Name { get; set; }
}

public class MYViewModel
{
    public ObservableCollection<NamedThing> ThingsList { get; set; }
         = new ObservableCollection<NamedThing>
        {
            new NamedThing {Name = "Short"},
            new NamedThing {Name = "VeryVeryLongWord"},
            new NamedThing {Name = "Several Words in a Row"}
        };
}

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
    }
}

Upvotes: 2

Views: 2266

Answers (1)

Kilazur
Kilazur

Reputation: 3188

The problem is that your Viewbox scale whatever is inside it, but the TextBlocks don't know in what they are contained and just grow (width wise) as much as they're allowed to (which is Infinity by default).

A first and quick solution is to set a MaxWidth (100, for example) value to the TextBlock. This way, the multi-word TextBlock will simply react exactly as the single-word ones: it will grow and shrink, but the word wrapping won't change.

The reason MaxWidth works and not Width is logically obvious when you know how Viewbox works: single words TextBlocks are smaller, so their Width is smaller, so they grow much more in a Viewbox than their multi-words counterparts, making them appear in a bigger font.


Expanding on this solution, you can bind MaxWidth to one of the TextBlock's parent's MaxWidth to have a evolving word-wrapping when the window is resized. You can then add a converter that will modify the value (like, divide it by 2) if you feel like the multi-word TextBlock doesn't take enough vertical space.

Code-behind converter:

public class WidthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return ((double)value) / 2.0;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return ((double)value) * 2.0;
    }
}

XAML resources:

<Window.Resources>
    <local:MYViewModel x:Key="myVM" />
    <local:WidthConverter x:Key="wc" />
</Window.Resources>

MaxWidth:

<TextBlock TextWrapping="Wrap"
    MaxWidth="{Binding ActualWidth, Converter={StaticResource wc}, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
    Text="{Binding Name}" />

The following example is a bit more complicated and more 'proof-of-concept' than solution; we split the multi-words with a converter to display each single word in a slot of a templated UniformGrid; the behavior seems more natural, but the layout of multi-word strings is kind of gross.

enter image description here

XAML:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication2"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="150"
        Width="525">
    <Window.Resources>
        <local:MYViewModel x:Key="myVM" />
        <local:ThingConverter x:Key="tc" />
    </Window.Resources>
    <Grid  DataContext="{Binding Source={StaticResource myVM}}">
        <ItemsControl ItemsSource="{Binding ThingsList, Mode= OneWay}"
                      HorizontalAlignment="Stretch">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="3"
                                 Rows="1"
                                 HorizontalAlignment="Stretch" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button>
                        <Viewbox>
                            <ItemsControl ItemsSource="{Binding Converter={StaticResource tc}}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <UniformGrid />
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <TextBlock HorizontalAlignment="Center"
                                                   VerticalAlignment="Center"
                                                   TextAlignment="Center"
                                                   Text="{Binding}" />
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </Viewbox>
                    </Button>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

Code-behind:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication2
{
    public class ThingConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return new List<string>(((NamedThing)value).Name.Split(' '));
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((List<string>)value).Aggregate((s, ss) => s + " " + ss);
        }
    }

    public class NamedThing
    {
        public string Name { get; set; }
    }

    public class MYViewModel
    {
        public ObservableCollection<NamedThing> ThingsList { get; set; }

        public MYViewModel()
        {
            ThingsList = new ObservableCollection<NamedThing>
            {
                new NamedThing {Name = "Short"},
                new NamedThing {Name = "VeryVeryLongWord"},
                new NamedThing {Name = "Several Words in a Row"}
            };
        }
    }

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

Upvotes: 2

Related Questions