want to make scrollable tabs for a tabcontrol - wpf

Say I have a tab control, and I have over 50 tabs, where there is no enough space to hold so many tabs, how make these tabs scrollable?

Rick's answer actually breaks the vertical stretching of content inside the tabcontrol. It can be improved to retain vertical stretching by using a two row grid instead of a StackPanel.
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden" >
<TabPanel x:Name="HeaderPanel"
Panel.ZIndex ="1"
KeyboardNavigation.TabIndex="1"
Grid.Column="0"
Grid.Row="0"
Margin="2,2,2,0"
IsItemsHost="true"/>
</ScrollViewer>
<ContentPresenter x:Name="PART_SelectedContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Margin="{TemplateBinding Padding}"
ContentSource="SelectedContent" Grid.Row="1"/>
</Grid>
</ControlTemplate>
</TabControl.Template>

Override the TabControl ControlTemplate and add a ScrollViewer around the TabPanel like this sample:
<Grid>
<TabControl>
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Disabled">
<TabPanel x:Name="HeaderPanel"
Panel.ZIndex ="1"
KeyboardNavigation.TabIndex="1"
Grid.Column="0"
Grid.Row="0"
Margin="2,2,2,0"
IsItemsHost="true"/>
</ScrollViewer>
<ContentPresenter x:Name="PART_SelectedContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Margin="{TemplateBinding Padding}"
ContentSource="SelectedContent"/>
</StackPanel>
</ControlTemplate>
</TabControl.Template>
<TabItem Header="TabItem1">TabItem1 Content</TabItem>
<TabItem Header="TabItem2">TabItem2 Content</TabItem>
<TabItem Header="TabItem3">TabItem3 Content</TabItem>
<TabItem Header="TabItem4">TabItem4 Content</TabItem>
<TabItem Header="TabItem5">TabItem5 Content</TabItem>
<TabItem Header="TabItem6">TabItem6 Content</TabItem>
<TabItem Header="TabItem7">TabItem7 Content</TabItem>
<TabItem Header="TabItem8">TabItem8 Content</TabItem>
<TabItem Header="TabItem9">TabItem9 Content</TabItem>
<TabItem Header="TabItem10">TabItem10 Content</TabItem>
</TabControl>
</Grid>
which gives this result:

Recently I've implemented such control. It contains two buttons (to scroll left and right) which switch their IsEnabled and Visibility states when it is necessary. Also it works perfectly with item selection: if you select a half-visible item, it will scroll to display it fully.
It looks so:
It isn't so much different from the default control, the scrolling is appeared automatically:
<tab:ScrollableTabControl ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
IsAddItemEnabled="False"
.../>
I've written the article about this ScrollableTabControl class in my blog here.
Source code you can find here: WpfScrollableTabControl.zip

The above solution is great for tab items with the tab control's "TabStripPlacement" property set to "Top". But if you are looking to have your tab items, say to the left side, then you will need to change a few things.
Here is a sample of how to get the scrollviewer to work with the TabStripPlacement to the Left:
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ScrollViewer
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
FlowDirection="RightToLeft">
<TabPanel
x:Name="HeaderPanel"
Panel.ZIndex ="0"
KeyboardNavigation.TabIndex="1"
IsItemsHost="true"
/>
</ScrollViewer>
<ContentPresenter
x:Name="PART_SelectedContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
ContentSource="SelectedContent" Grid.Column="1"
/>
</Grid>
</ControlTemplate>
Note that in the ScrollViewer I set FlowDirection="RightToLeft" so that the scroll bar would snap to the left of the tab items. If you are placing your tab items to the right the you will need to remove the FlowDirection property so that it defaults to the right side.
And here is the result:

For thoose who want to know how to make the scrollviewer scroll to the selected tab item.
Add this event SelectionChanged="TabControl_SelectionChanged" to your TabControl.
Then give a name like TabControlScroller to the ScrollViewer inside the template. You should end with something like this
<TabControl SelectionChanged="TabControl_SelectionChanged">
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ScrollViewer x:Name="TabControlScroller" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" >
<TabPanel x:Name="HeaderPanel"
Panel.ZIndex ="1"
KeyboardNavigation.TabIndex="1"
Grid.Column="0"
Grid.Row="0"
Margin="2,2,2,0"
IsItemsHost="true"/>
</ScrollViewer>
<ContentPresenter x:Name="PART_SelectedContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Margin="{TemplateBinding Padding}"
ContentSource="SelectedContent" Grid.Row="1"/>
</Grid>
</ControlTemplate>
</TabControl.Template>
<!-- Your Tabitems-->
</TabControl>
Then in code behind you just have to add this method :
private void TabControl_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
TabControl tabControl = (TabControl)sender;
ScrollViewer scroller = (ScrollViewer)tabControl.Template.FindName("TabControlScroller", tabControl);
if (scroller != null)
{
double index = (double)(tabControl.SelectedIndex );
double offset = index * (scroller.ScrollableWidth / (double)(tabControl.Items.Count));
scroller.ScrollToHorizontalOffset(offset);
}
}

Place it inside a ScrollViewer.
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden">
<TabControl ...>
...
</TabControl>
</ScrollViewer>

Related

XAML: How to bind TabControl nested element to Attached Property of TabItem

I have been searching for a "pure" XAML solution for this problem but just cannot find it.
My goal would be to only create an Attached Property in code behind but the rest should be XAML only without creating a Custom Control or User Control. But I'm not sure whether this is possible at all and if, how to make the connection between a nested element inside the TabControl template and an Attached Property set in a TabItem
I'd have a boilerplate Attached Property of string with [AttachedPropertyBrowsableForType(typeof(TabItem))] and/or [AttachedPropertyBrowsableForType(typeof(TabControl))] inside my MainWindow class and the following XAML
<Window x:Class="AP_Test.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:AP_Test"
mc:Ignorable="d"
Title="MainWindow" Height="400" Width="800">
<Window.Resources>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition Name="ColumnDefinition0"/>
<ColumnDefinition Name="ColumnDefinition1" Width="0"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Name="RowDefinition0" Height="Auto"/>
<RowDefinition Name="RowDefinition1" Height="*"/>
</Grid.RowDefinitions>
<TabPanel Name="headerPanel"
Background="Transparent"
Grid.Column="0"
IsItemsHost="true"
Margin="2,2,2,0"
Grid.Row="0"
KeyboardNavigation.TabIndex="1"
Panel.ZIndex="1"/>
<Border Name="contentPanel"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="0"
KeyboardNavigation.DirectionalNavigation="Contained"
Grid.Row="1"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<DockPanel Background="White">
<Grid Name="TabControlHeader" DockPanel.Dock="Top" Height="65">
<Label x:Name="SelectedItemTitle" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" Content="How to bind to AP ItemTitle?"/>
</Grid>
<Grid Name="Detail" Margin="8,0,8,8">
<Border BorderThickness="3,3,0,0" BorderBrush="DarkGray" CornerRadius="3"/>
<Border BorderThickness="2,2,1,1" BorderBrush="LightGray" CornerRadius="3"/>
<Border BorderThickness="1,1,1,1" BorderBrush="White" CornerRadius="3" Margin="3,3,-1,-1" Padding="5">
<Viewbox>
<ContentPresenter Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Viewbox>
</Border>
</Grid>
</DockPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<TabControl x:Name="TabCtl">
<TabItem Header="Tab1" local:MainWindow.ItemTitle="Tab1 Title" />
<TabItem Header="Tab2" local:MainWindow.ItemTitle="Tab2 Title" />
<TabItem Header="Tab3" local:MainWindow.ItemTitle="Tab3 Title" />
</TabControl>
</Window>
I'd like the respective title entries to be displayed in the TabControl's SelectedItemTitle label.
Any hints appreciated, even a definitive "That's not possible" would be good to know, so I can stop trying 😁
The property (sub-)path for an attached property needs to be enclosed in parentheses:
Content="{Binding Path=SelectedItem.(local:MainWindow.ItemTitle),
RelativeSource={RelativeSource AncestorType=TabControl}}"
See PropertyPath for Objects in Data Binding for details.
An attached property is not even required. You could as well use the TabItem's Tag property like
<TabItem Header="Tab1" Tag="Tab1 Title"/>
with
Content="{Binding Path=SelectedItem.Tag,
RelativeSource={RelativeSource AncestorType=TabControl}}"

Issue with WPF WrapPanel inside a ScrollViewer

There are a number of similar questions on SO but so far I have not been able to resolve my problem using them.
I have a bunch of Controls inside a WrapPanel and the WrapPanel is inside a ScrollViewer. The ScrollViewer is inside a Grid.
I am trying to get all the <Border> controls in the WrapPanel to have an Orientation of 'Vertical' (so that they flow down and when there is no more space left vertically they wrap horizontally) with a HorizontalScrollBar that appears when there is no more space left Horizontally.
My code so far is as follows:
<Grid x:Name="configGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" Width="{Binding ElementName=configGrid, Path=ActualWidth}" Height="{Binding ElementName=configGrid, Path=ActualHeight}">
<WrapPanel HorizontalAlignment="Left" Orientation="Vertical" x:Name="ConfigWrapPanel" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollViewer}}, Path=ActualWidth}">
<Border BorderBrush="White" BorderThickness="1,1,1,1" CornerRadius="2" Margin="10">
<Expander IsExpanded="True" BorderThickness="0" Header="General">
// some controls here
</Expander>
</Border>
<Border BorderBrush="White" BorderThickness="1,1,1,1" CornerRadius="2" Margin="10">
<Expander IsExpanded="True" BorderThickness="0" Header="Another Block">
// some controls here
</Expander>
</Border>
// many more <border> blocks here....
</WrapPanel>
</ScrollViewer>
</Grid>
This almost works as expected, the various content flows vertically and when there is not enough room at the bottom it moves up and right and starts at the top again. But I never get any horizontal scrollbars and the controls just disappear off the right of the screen.
I'm sure this is something really simple I'm missing but I can't quite figure it out.
As a bit of further info, the various Border controls and sub elements are all of dynamic width and height (which is why I opted for a vertical orientation WrapPanel rather than Horizontal)
Any help would be greatly appreciated.
You have to remove the Width from your WrapPanel.
That width wants to stretch to infinity, which prevents the ScrollViewer from corrent measuring the boundaries of the WrapPanel resulting in never showing the ScrollBar.
Code below shows a working example:
<Grid x:Name="configGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" Height="{Binding ElementName=configGrid, Path=ActualHeight}">
<WrapPanel HorizontalAlignment="Left" Orientation="Vertical" x:Name="ConfigWrapPanel" >
<Border BorderBrush="White" BorderThickness="1,1,1,1" CornerRadius="2" Margin="10">
<Expander IsExpanded="True" BorderThickness="0" Header="General">
// some controls here
</Expander>
</Border>
<Border BorderBrush="White" BorderThickness="1,1,1,1" CornerRadius="2" Margin="10">
<Expander IsExpanded="True" BorderThickness="0" Header="Another Block">
// some controls here
</Expander>
</Border>
</WrapPanel>
</ScrollViewer>
</Grid>

SmallChange in a XAML ScrollViewer ControlTemplate has no effect

I've created a basic Windows Desktop WPF Application. In the MainWindow, I've added the following as the body of the window:
<ScrollViewer Template="{DynamicResource ScrollViewerControlTemplate1}">
<ScrollViewer.Resources>
<ControlTemplate x:Key="ScrollViewerControlTemplate1" TargetType="{x:Type ScrollViewer}">
<Grid x:Name="Grid" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Corner" Grid.Column="1" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Grid.Row="1"/>
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" Margin="{TemplateBinding Padding}" Grid.Row="0"/>
<ScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}" SmallChange="40000"/>
<ScrollBar x:Name="PART_HorizontalScrollBar" AutomationProperties.AutomationId="HorizontalScrollBar" Cursor="Arrow" Grid.Column="0" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Orientation="Horizontal" Grid.Row="1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}"/>
</Grid>
</ControlTemplate>
</ScrollViewer.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
<RowDefinition Height="300"/>
<RowDefinition Height="300"/>
<RowDefinition Height="300"/>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="SteelBlue"/>
<Border Grid.Row="1" Background="Peru"/>
<Border Grid.Row="2" Background="Goldenrod"/>
<Border Grid.Row="3" Background="Tomato"/>
<Border Grid.Row="4" Background="IndianRed"/>
</Grid>
</ScrollViewer>
You'll notice that on PART_VerticalScrollbar, I've set the SmallChange="40000" (an arbitrarily large number). Yet when I click the up/down arrows on the scrollbar, it does the same very small change as it did before I set the SmallChange to anything.
I've read over the documentation a number of times, and can't figure out why this isn't having any effect on the amount that the ScrollViewer scrolls. Any ideas?
Note that I could change the ScrollBar template and change the command these button calls to Scrollbar.PageUpCommand rather than Scrollbar.LineUpCommand, but ultimately I'd like to have finer control over the scrolling than a full page.
The reason for this is a special logic implemented in the ScrollViewer and in the ScrollBar.
The ScrollBar.SmallChange property will be only considered for scrolling if the scroll bar is stand-alone. That means, when it is outside of a ScrollViewer.
If you look at the ScrollViewer.OnApplyTemplate method, you will notice the following:
public override void OnApplyTemplate()
{
// ...
ScrollBar scrollBar = GetTemplateChild(HorizontalScrollBarTemplateName) as ScrollBar;
if (scrollBar != null)
scrollBar.IsStandalone = false;
// Same for the vertical scroll bar
// ...
}
If a ScrollViewer finds scroll bars inside of it, it sets their IsStandalone (internal) properties to false, which disables the scroll command handing of the scroll bars. Instead, the ScrollViewer takes over the scroll command handing, but it ignores some ScrollBar's properties, including the SmallChange property.
TL;DR This won't work for the scroll bars inside of a ScrollViewer. You can change your template so use two "external" scroll bars. But then, you will need to connect those scroll bars with the ScrollViewer manually.
I suspect that your custom control template isn't being used because of the way you are referencing it. I would change the XAML to this:
<ScrollViewer>
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid ...>
...
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
... ScrollViewer content goes here
</ScrollViewer>
Basically, set the control's template directly, rather than using a dynamic resource.

Add ControlTemplate to AnimatedTabControl without overwriting animation behavior

I am using MahApps AnimatedTabControl and I need to create a ControlTemplate to add a ScrollViewer for header tabs. Here is my template:
<TabControl.Template>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ScrollViewer x:Name="_MainTabControlScrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
<TabPanel x:Name="HeaderPanel" IsItemsHost="True" Margin="0,4,0,0"/>
</ScrollViewer>
<ContentPresenter x:Name="PART_SelectedContentHost" Margin="4" ContentSource="SelectedContent" Grid.Row="1"/>
</Grid>
</ControlTemplate>
</TabControl.Template>
However, this kills the animation. Is there a way to inherit the default AnimatedTabControl behavior?
Instead overriding the TabControl just use the MetroAnimatedSingleRowTabControl.
<Controls:MetroAnimatedSingleRowTabControl x:Name="AnimatedTabControl">
<TabItem Header="tab test"></TabItem>
</Controls:MetroAnimatedSingleRowTabControl>
with xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
Hope that helps.

Border Styling applied to all the child elements recursively

I am new to WPF and need your help in resolving my styling issue.
I have applied border styling to GRID as below
<Border CornerRadius="5" BorderBrush="Gainsboro" BorderThickness="1,1,0,0" Name="border1" Margin="90,54,20,50" >
<Border BorderBrush="Gray" CornerRadius="5" BorderThickness="0,0,1,1" >
<Border.Effect>
<DropShadowEffect BlurRadius="10" Direction="-50" ShadowDepth="7" />
</Border.Effect>
<Border.Child>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="356*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="446*" />
</Grid.ColumnDefinitions>
<TextBox Name="TB1" Style="{StaticResource CustomTextBoxStyle}" Grid.Column="1" Margin="46,79,400,277" Grid.Row="1" />
<ComboBox Height="24" Name="comboBox1" Width="110" Grid.Column="1" Margin="304,86,232,276" Grid.Row="1" />
</Grid>
</Border.Child>
</Border>
</Border>
Then I have placed text box and combo box in the grid with custom styling.
The problem is parent GRID's border style is applied to child TEXTBOX along with its own custom style properties.
Could you please help me out in this?
Thanks
Bharat
As per MSDN doucment -
When a BitmapEffect is applied to a layout container, such as
DockPanel or Canvas, the effect is applied to the visual tree of the
element or visual, including all of its child elements.
But, there is a workaround as described here and here to have another border with same position but without the effect, that would resolve the problem -
<Grid>
<Border Margin="10" BorderBrush="Red" BorderThickness="1">
<Border.Effect>
<DropShadowEffect Color="Gray"/>
</Border.Effect>
</Border>
<Border Margin="10">
<!-- controls -->
</Border>
</Grid>

Resources