Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Create a Wrapping Button in WPF

Requirements:

  • Must support the MinWidth property
  • Can display text on one or two lines only. Cannot wrap to 3 or more.
  • Should make button as small as possible while respecting MinWidth
  • Text can wrap only on spaces
  • Cannot specify width. The button should keep growing horizontally.

Here is what I would like the buttons to look like (Ignore the styling please, the wrapping is the important part)

enter image description here

I am looking for ideas on how to make the text wrap dynamically.

like image 698
Shaun Bowe Avatar asked Oct 10 '22 09:10

Shaun Bowe


2 Answers

I tried to achieve this by editing the default template for Button, mainly adding a wrapping TextBlock instead of the default ContentPresenter and calculate its Width in a converter. This approach needed pretty much data in the converter though, there's proabaly easier (and better :) ways to do this but it seems to be working anyway. It needs a reference to PresentationFramework.Aero because of the ButtonChrome in the default template

Just use it like this

<Button Style="{StaticResource WrappingButton}"
        MinWidth="100"
        Content="This button has some long text">
</Button>

enter image description here
Screenshot sample with 3 WrappingButtons in a StackPanel

WrappingButton Style

<Style x:Key="WrappingButton" TargetType="{x:Type Button}"
       xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero">
    <Setter Property="HorizontalAlignment" Value="Left"/>
    <Setter Property="Width" Value="Auto"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <ControlTemplate.Resources>
                    <local:WrappingButtonWidthConverter x:Key="WrappingButtonWidthConverter"/>
                </ControlTemplate.Resources>
                <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" RenderDefaulted="{TemplateBinding IsDefaulted}" SnapsToDevicePixels="true">
                    <TextBlock VerticalAlignment="Center"
                               FontSize="{TemplateBinding FontSize}"
                               FontFamily="{TemplateBinding FontFamily}"
                               FontStyle="{TemplateBinding FontStyle}"
                               FontWeight="{TemplateBinding FontWeight}"
                               FontStretch="{TemplateBinding FontStretch}"
                               LineStackingStrategy="BlockLineHeight"
                               TextWrapping="Wrap"
                               TextTrimming="WordEllipsis"
                               Text="{Binding RelativeSource={RelativeSource TemplatedParent},
                                              Path=Content}">
                        <TextBlock.Width>
                            <MultiBinding Converter="{StaticResource WrappingButtonWidthConverter}">
                                <Binding RelativeSource="{RelativeSource Self}" Path="Text"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontFamily"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStyle"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontWeight"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStretch"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontSize"/>
                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MinWidth"/>
                            </MultiBinding>
                        </TextBlock.Width>
                    </TextBlock>
                </Microsoft_Windows_Themes:ButtonChrome>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsKeyboardFocused" Value="true">
                        <Setter Property="RenderDefaulted" TargetName="Chrome" Value="true"/>
                    </Trigger>
                    <Trigger Property="ToggleButton.IsChecked" Value="true">
                        <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="#ADADAD"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

WrappingButtonWidthConverter

public class WrappingButtonWidthConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string text = values[0].ToString();
        FontFamily fontFamily = values[1] as FontFamily;
        FontStyle fontStyle = (FontStyle)values[2];
        FontWeight fontWeight = (FontWeight)values[3];
        FontStretch fontStretch = (FontStretch)values[4];
        double fontSize = (double)values[5];
        double minWidth = (double)values[6];

        string[] words = text.Split(new char[] {' '});
        double widthSum = 0.0;
        List<double> wordWidths = GetWordWidths(words, fontFamily, fontStyle, fontWeight, fontStretch, fontSize, out widthSum);

        double width = 0.0;
        for (int i = 0; width < (widthSum / 2.0) && i < wordWidths.Count; i++)
        {
            width += wordWidths[i];
        }

        return minWidth > 0.0 ? Math.Max(minWidth, width) : width;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    private List<double> GetWordWidths(string[] words,
                                       FontFamily fontFamily,
                                       FontStyle fontStyle,
                                       FontWeight fontWeight,
                                       FontStretch fontStretch,
                                       double fontSize,
                                       out double widthSum)
    {
        List<double> wordWidths = new List<double>();
        widthSum = 0.0;
        foreach (string word in words)
        {
            Typeface myTypeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
            FormattedText ft = new FormattedText(word + " ",
                                                 CultureInfo.CurrentCulture,
                                                 FlowDirection.LeftToRight,
                                                 myTypeface,
                                                 fontSize,
                                                 Brushes.Black);
            wordWidths.Add(ft.WidthIncludingTrailingWhitespace);
            widthSum += ft.WidthIncludingTrailingWhitespace;
        }
        return wordWidths;
    }
}
like image 170
Fredrik Hedblad Avatar answered Oct 14 '22 02:10

Fredrik Hedblad


Since you have limited the text to two lines, my suggestion would be to write a custom panel as the button content which turns the specified text, font, etc. into a WPF FormattedText object. You can then measure it and decide how you want it to layout and display in MeasureOverride and ArrangeOverride. FormattedText even has a parameter to display a ... abbreviation if the text doesn't fit. To keep it to two lines, you would want to first create it, then check its Height to see what a single line is for height. (need to add rest in comments as StackOverflow is tossing errors).

like image 23
Ed Bayiates Avatar answered Oct 14 '22 00:10

Ed Bayiates