5
votes

I want to have a TabControl with multiple TabItems. These TabItems each have a header text. These texts may vary a lot in length (like 5 chars long and 15 chars long).

I want the TabControl to align the headers in one row only.

All tab headers should use the same width, and when there is enough space available, i want them the to use all the space available, up to a MaxWidth, that is the same for all items.

So if i want to use vMaxWidth` of 100 for 7 items, the tab header should be max 700 in width. If there is more space available, it should be ignored.

If there is less space available, i want that space to be distributed equally between the items. If the text gets cut off, i want to use TextWrapping.

I have tried multiple approaches to this problem now, this is my current setup:

<Style x:Key="Style-TabControl-Main" TargetType="{x:Type TabControl}">
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabControl}">
                <Grid KeyboardNavigation.TabNavigation="Local">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>

                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>

                    <Border BorderThickness="0,0,0,1" Margin="13,0,0,0" BorderBrush="{StaticResource Brush-White}">
                        <StackPanel Panel.ZIndex="1" x:Name="HeaderPanel" IsItemsHost="True" KeyboardNavigation.TabIndex="1" Background="Transparent" 
                                    Orientation="Horizontal"/>
                    </Border>

                    <Border x:Name="Border"
                            Grid.Row="1" Grid.ColumnSpan="2"
                            KeyboardNavigation.TabNavigation="Local"
                            KeyboardNavigation.DirectionalNavigation="Contained"
                            KeyboardNavigation.TabIndex="2"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}">
                        <ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" />
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And the TabItem Style

<Style x:Key="Style-TabItem-Main" TargetType="{x:Type TabItem}">
    <Setter Property="Height" Value="31"/>
    <Setter Property="Width" Value="180" />
    <Setter Property="Foreground" Value="{DynamicResource Brush-BrightRegular-Foreground}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <Border x:Name="Border" Cursor="Hand" 
                        Margin="2,0,0,0"
                        BorderThickness="1,1,1,0"
                        CornerRadius="4,4,0,0"
                        BorderBrush="{DynamicResource Brush-BrightRegular-Background}"
                        Background="{DynamicResource Brush-White}">
                    <ContentPresenter x:Name="Content" VerticalAlignment="Center" HorizontalAlignment="Stretch" ContentSource="Header" RecognizesAccessKey="True"
                                        TextBlock.TextAlignment="Center" TextBlock.FontSize="16" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected"   Value="True">
                        <Setter  Property="Foreground" Value="{DynamicResource Brush-White}"/>
                        <Setter TargetName="Border"  Property="Background" Value="{DynamicResource Brush-DefaultDark-Background}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I am using a StackPanel instead of a TabPanel to get rid of the "stacking", that occurs, when you resize a default TabControl. However, i cannot get the rest of my requirements to work. I tried applying a MaxWidth (instead of fixed width) to the TabItem headers, but that of course doesn't work, because the item than shrinks to its minimum required size.

2
I have worked on TabControl some time ago so I can't guarantee that this will work but, firstly you don't need <Style x:Key="Style-TabItem-Main" TargetType="{x:Type TabItem}"> a simple <Style TargetType="{x:Type TabItem}"> will do, secondly remove the VerticalAlignment and HorizontalAlignment from your ContentControl, from I have noticed this will mess up the logic for stretching the content. BTW try to wrap the ContentPresenter in DockPanel. HTHXAMlMAX
I actually need to key, because i am using multiple different TabControl and TabItem styles. This is just one of them!user604613

2 Answers

9
votes

Step 1 (first attempt): Put headers in a single row, and give each header the same width.

This can be achieved by using a UniformGrid instead of the standard TabPanel, and lock its row count to 1. Here is a stripped-down version of your TabControl style:

<Style x:Key="Style-TabControl-Main" TargetType="{x:Type TabControl}">
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabControl}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>

                    <Border>
                        <UniformGrid x:Name="HeaderPanel" IsItemsHost="True" 
                                     Rows="1" />
                    </Border>

                    <Border x:Name="Border" Grid.Row="1" 
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}">
                        <ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" />
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Step 2: Restrict headers to a MaxWidth and apply text wrapping.

The MaxWidth can be set in the TabItem style, along with a HeaderTemplate which wraps text (you can still use your custom ControlTemplate here to style the TabItem parts):

<Style x:Key="Style-TabItem-Main" TargetType="{x:Type TabItem}">
    <Setter Property="MaxWidth" Value="100" />
    <!--https://social.msdn.microsoft.com/forums/vstudio/en-US/df4f7fc3-f0ec-4ed1-a022-a32650e49cb3/how-to-wrap-header-text-in-tabcontrol-->
    <Setter Property="HeaderTemplate" >
        <Setter.Value>
            <DataTemplate>
                <TextBlock Text="{Binding}" TextWrapping="Wrap" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        ...
    </Setter>
</Style>


Troubleshooting: Now, if you apply the MaxWidth in Step 2, you'll probably want to left-align the UniformGrid when the TabControl gets too wide..

<UniformGrid x:Name="HeaderPanel" IsItemsHost="True" 
             Rows="1" HorizontalAlignment="Left" />

..but you don't want that when the MaxWidth hasn't been reached yet, and the items should stretch across the entire width of the TabControl (aka Step 1). So we need a way to switch that HorizontalAlignment depending on whether the items' MaxWidth (if set) has been reached.

Step 1 (revisited): Let's try to make our own UniformGrid:

public class UniformTabPanel : UniformGrid
{
    public UniformTabPanel()
    {
        this.IsItemsHost = true;
        this.Rows = 1;

        //Default, so not really needed..
        this.HorizontalAlignment = HorizontalAlignment.Stretch;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var totalMaxWidth = this.Children.OfType<TabItem>().Sum(tab => tab.MaxWidth);
        if (!double.IsInfinity(totalMaxWidth))
        {
            this.HorizontalAlignment = (constraint.Width > totalMaxWidth) 
                                                ? HorizontalAlignment.Left 
                                                : HorizontalAlignment.Stretch;
        }

        return base.MeasureOverride(constraint);
    }
}

Now, we can replace the UniformGrid in our TabControl style this new panel:

                ...
                    <Border>
                        <mycontrols:UniformTabPanel x:Name="HeaderPanel" />
                    </Border>
                ...

...and the TabControl should function as expeced.

0
votes

Sphinxx answer is correct, however i needed to add the following code to the UniformTabPanel, to make it work like i want (resize the headers to maxwidth when enough space is available)

I added the following code to the UniformTabPanel, and it now does what i need:

protected override Size MeasureOverride(Size constraint)
{
    var children = this.Children.OfType<TabItem>();
    var totalMaxWidth = children.Sum(tab => tab.MaxWidth);
    if (!double.IsInfinity(totalMaxWidth))
    {
        this.HorizontalAlignment = (constraint.Width > totalMaxWidth)
                                            ? HorizontalAlignment.Left
                                            : HorizontalAlignment.Stretch;
        foreach (var child in children)
        {
            child.Width = this.HorizontalAlignment == System.Windows.HorizontalAlignment.Left
                    ? child.MaxWidth
                    : Double.NaN;
        }
    }
    return base.MeasureOverride(constraint);
}