I am trying to make a vertical TabControl section on a WPF app and would like the Header buttons to fill the height of the screen (down to a MinHeight) and be of uniform size. I am hoping that this can be done through creating a custom template for the TabControl and TabItem templates.
Tab control template
<Style TargetType="{x:Type TabControl}">
<Setter Property="Padding" Value="2"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{StaticResource TabItem.Selected.Background}"/>
<Setter Property="BorderBrush" Value="{StaticResource TabItem.Selected.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid x:Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0"/>
<ColumnDefinition x:Name="ColumnDefinition1"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto"/>
<RowDefinition x:Name="RowDefinition1" Height="*"/>
</Grid.RowDefinitions>
<TabPanel x:Name="headerPanel" Background="Transparent" Grid.Column="0" IsItemsHost="true" Margin="2,2,2,0" Grid.Row="0" KeyboardNavigation.TabIndex="1" Panel.ZIndex="1"/>
<Border x:Name="contentPanel" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="0" KeyboardNavigation.DirectionalNavigation="Contained" Grid.Row="1" KeyboardNavigation.TabIndex="2" KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement" Value="Left">
<Setter Property="Width" TargetName="headerPanel" Value="80"/>
<Setter Property="Grid.Row" TargetName="headerPanel" Value="0"/>
<Setter Property="Grid.Row" TargetName="contentPanel" Value="0"/>
<Setter Property="Grid.Column" TargetName="headerPanel" Value="0"/>
<Setter Property="Grid.Column" TargetName="contentPanel" Value="1"/>
<Setter Property="Width" TargetName="ColumnDefinition0" Value="Auto"/>
<Setter Property="Width" TargetName="ColumnDefinition1" Value="*"/>
<Setter Property="Height" TargetName="RowDefinition0" Value="*"/>
<Setter Property="Height" TargetName="RowDefinition1" Value="0"/>
<Setter Property="Margin" TargetName="headerPanel" Value="2,2,0,2"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="TextElement.Foreground" TargetName="templateRoot" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I have been able to manually set the heights of each TabItem header by setting the properties, however this then isn't dynamic as the user changes the window size
You should not use a UniformGrid
to replace the TabPanel
because you would lose relevant features like proper multi-line tab item wrapping and other TabControl
specific item layout behaviors. Both UnformGrid
and TabPanel
use completely different algorithms to arrange their children. UnformGrid
does not explicitly honor the nature of the TabControl
.
If you don't care about losing features or the slightly different layout, then the simplest solution would be to simply replace the TabPanel
with a UniformGrid
. Because you want to stack the TabItem
elements vertically, you would lose the multi-line feature anyways (multi-line arrangement is by default only available for horizontal alignment i.e. TabStripPlacement
is set to Dock.Top
or Dock.Bottom
).
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid x:Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0"/>
<ColumnDefinition x:Name="ColumnDefinition1"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto"/>
<RowDefinition x:Name="RowDefinition1" Height="*"/>
</Grid.RowDefinitions>
<UniformGrid x:Name="headerPanel"
Background="Transparent"
Grid.Column="0"
IsItemsHost="true"
Margin="2,2,2,0"
Grid.Row="0"
KeyboardNavigation.TabIndex="1"
Panel.ZIndex="1" />
<Border x:Name="contentPanel"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Grid.Column="0"
KeyboardNavigation.DirectionalNavigation="Contained"
Grid.Row="1"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
</ControlTemplate>
A cleaner solution, that respects the special layout requirements of the TabControl
is to extend the TabPanel
.
in the following example the new class UniformTabPanel
extends TabPanel
and overrides the layout arrangement for the vertical arrangement.
For the horizontal arrangement the UniformTabPanel
panel will fall back to the default layout algorithm of the TabPanel
.
To use the UniformTabPanel
simply replace the TabPanel
in the ControlTemplate
of the TabControl
:
UniformTabPanel.cs
public class UniformTabPanel : TabPanel
{
protected override Size ArrangeOverride(Size arrangeSize)
{
Dock itemPlacement = this.TemplatedParent is TabControl tabControl
? tabControl.TabStripPlacement
: Dock.Left; // Prefer vertical stacking for this panel
if (itemPlacement is Dock.Left or Dock.Right)
{
ArrangeVertical(arrangeSize);
return arrangeSize;
}
else
{
// Apply default layout behavior
return base.ArrangeOverride(arrangeSize);
}
}
private void ArrangeVertical(Size arrangeSize)
{
double uniformDesiredChildHeight = GetUniformItemHeight(arrangeSize);
double childOffsetY = 0d;
foreach (UIElement child in this.InternalChildren)
{
if (child.Visibility != Visibility.Collapsed)
{
Size childSize = GetDesiredSizeWithoutMargin(uniformDesiredChildHeight, child);
child.Arrange(new Rect(0, childOffsetY, arrangeSize.Width, childSize.Height));
// Calculate the offset for the next child
childOffsetY += childSize.Height;
}
}
}
private double GetUniformItemHeight(Size arrangeSize)
{
double totalDesiredHeight = this.TemplatedParent is TabControl tabControl
? tabControl.DesiredSize.Height
: arrangeSize.Height;
return totalDesiredHeight / Math.Max(1, this.InternalChildren.Count);
}
private Size GetDesiredSizeWithoutMargin(double desiredChildHeight, UIElement element)
{
Thickness margin = (Thickness)element.GetValue(MarginProperty);
Size desiredSizeWithoutMargin = new Size
{
Height = Math.Max(0d, desiredChildHeight - margin.Top - margin.Bottom),
Width = Math.Max(0d, element.DesiredSize.Width - margin.Left - margin.Right)
};
return desiredSizeWithoutMargin;
}
}
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid x:Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0"/>
<ColumnDefinition x:Name="ColumnDefinition1"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto"/>
<RowDefinition x:Name="RowDefinition1" Height="*"/>
</Grid.RowDefinitions>
<local:UniformTabPanel x:Name="headerPanel"
Background="Transparent"
Grid.Column="0"
IsItemsHost="true"
Margin="2,2,2,0"
Grid.Row="0"
KeyboardNavigation.TabIndex="1"
Panel.ZIndex="1" />
<Border x:Name="contentPanel"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Grid.Column="0"
KeyboardNavigation.DirectionalNavigation="Contained"
Grid.Row="1"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
</ControlTemplate>