WPF Standard Control Demo App › TabControl

TabControl Selectors

TabControl shows one page at a time and a strip of tabs to switch between them. Each page is a TabItem.

Where the tabs go, and how much room the page keeps

TabStripPlacement decides the edge a TabControl puts its tabs on; the default is Top. In a TabControl 300 × 150, Top and Bottom put the two tabs side by side, with the content area 294 wide below or above them. Left and Right stacked the tabs, each about 37 wide and 20 high, with their text still horizontal. The content area narrowed to 255.34.

TabStripPlacement Tab1 header Tab2 header content area
Top x=0 y=0 w=40.66 h=21.96 x=38.66 y=2 w=36.66 h=19.96 x=3 y=24.96 w=294 h=122.04
Bottom x=0 y=128.04 w=40.66 h=21.96 x=38.66 y=128.04 w=36.66 h=19.96 x=3 y=3 w=294 h=122.04
Left x=0 y=0 w=38.66 h=23.96 x=2 y=21.96 w=36.66 h=19.96 x=41.66 y=3 w=255.34 h=144
Right x=261.34 y=0 w=38.66 h=23.96 x=261.34 y=21.96 w=36.66 h=19.96 x=3 y=3 w=255.34 h=144

Tab headers and the content area for each TabStripPlacement. Measured on .NET 10 / Windows 11.

Which tab is selected, and what is ignored

TabControl derives from Selector. A new TabControl had SelectedIndex -1 until it was shown, and then the first tab was selected. The tabs can be TabItem elements in Items or come from ItemsSource, for example one tab per open document, but not both: setting ItemsSource when Items already held TabItems, and adding to Items when ItemsSource was set, both threw InvalidOperationException.

An index past the last tab and an item not in the list are ignored rather than rejected. With two tabs, SelectedIndex = 5 threw nothing and left the index at 0, and setting SelectedItem to a TabItem not in the list left the second tab selected. A value below -1 is rejected: SelectedIndex = -2 threw ArgumentException. Check indexes before setting them. With SelectedIndex = -1, no page was shown. When the selected tab was removed, the next one was selected: removing the second of three tabs selected the third, at index 1.

The keyboard moves the selection with the focus: with the focus on the first tab’s header, the right arrow selected the second tab and focused its header. How the pages’ content is created and kept is measured on the TabItem page.

ContentStringFormat, and a tab’s own format

ContentStringFormat formats the content of the pages. With "Format: {0}.", the page of Tab1 showed “Format: Item1.”. A TabItem’s own ContentStringFormat is used when that tab is selected from the start: Tab2 with "Own {0}" showed “Own Item2”. Switching to it from Tab1 did not apply it, though: the page showed “Format: Item2.”, while SelectedContentStringFormat already read "Own {0}". Set one ContentStringFormat on the TabControl, or use templates, rather than different formats per TabItem.

SelectedContent and SelectedContentStringFormat are read-only and give the content and the format of the selected page. With the first tab selected, they were Item1, the content before formatting, and "Format: {0}.". With no tab selected, SelectedContent was null.

case measured
base class / default TabStripPlacement / first Dock value (demo start) Selector / Top / Left
SelectedIndex before showing / after showing -1 / 0
demo "Format: {0}.": text / SelectedContent / SelectedContentStringFormat Format: Item1. / Item1 / Format: {0}.
switched to Tab2 (its own "Own {0}"): text / SelectedContentStringFormat Format: Item2. / Own {0}
Tab2 selected before showing: text / SelectedContentStringFormat Own Item2 / Own {0}
2 tabs, SelectedIndex = 5: exception / SelectedIndex no exception / 0
SelectedIndex = -2: exception / SelectedIndex ArgumentException / 0
Tab2 selected, SelectedItem = a TabItem not in the list 1
SelectedIndex = -1: SelectedContent / text shown null / (nothing)
3 tabs, Tab2 selected and removed: SelectedIndex / selected tab 1 / Tab3
Items filled, then ItemsSource / ItemsSource, then Items.Add InvalidOperationException / InvalidOperationException
focus on Tab1's header, Right arrow: SelectedIndex / focused header 1 / Tab2

String format, selection, items, and the keyboard. Measured on .NET 10 / Windows 11.

Trying it in the demo app

The TabControl page of the demo app, with the control list on the left and the first section, TabStripPlacement

The TabControl page of the demo app has sections for TabStripPlacement, ContentStringFormat, and SelectedContent with SelectedContentStringFormat. The TabStripPlacement combo box starts at Left, the first value of the Dock enumeration, not at the default Top, so set it explicitly if you copy the XAML. The ContentStringFormat section uses "Format: {0}.", and the last section shows SelectedContent and SelectedContentStringFormat. The “Show Code” link under each section displays its XAML. The following XAML is the SelectedContent section (TabControlUsageControl.xaml), with the styles and the surrounding GroupBoxes left out and the namespace declarations added:

<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <TabControl x:Name="SelectedContentTabControl" ContentStringFormat="Format: {0}.">
    <TabItem Content="Item1" Header="Tab1" />
    <TabItem Content="Item2" Header="Tab2" />
  </TabControl>

  <TextBlock x:Name="SelectedContentTextBlock"
             Text="{Binding SelectedContent, ElementName=SelectedContentTabControl}" />
  <TextBlock x:Name="SelectedContentStringFormatTextBlock"
             Text="{Binding SelectedContentStringFormat, ElementName=SelectedContentTabControl}" />
</StackPanel>

Source code and how it was measured

Every behavior on this page was measured by running it on .NET 10 / Windows 11, using TabControlDemoScene in this site’s screenshot tool, with tabs like the demo app’s (Tab1 / Item1). The text shown was read from the template’s PART_SelectedContentHost, and the arrow key was sent through WPF’s input manager, the same path as keys typed on a keyboard. Sizes are the values on the measuring machine.

View TabControl source code on GitHub →