Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
06234fb
Add Tabs
Koichi-Kobayashi Nov 25, 2025
3276869
Add drag-and-drop support to Standard TabControl in Gallery
Koichi-Kobayashi Nov 25, 2025
3296f0e
Prevented errors when PreviewMouseMove is called during dragging
Koichi-Kobayashi Nov 25, 2025
6cf30bb
Reorder tabs: move from original position to target position
Koichi-Kobayashi Nov 26, 2025
39abcb6
MVVM compliant
Koichi-Kobayashi Nov 26, 2025
4a249fb
Optimization
Koichi-Kobayashi Nov 27, 2025
1dc39a0
Custom Control Conversion Part1
Koichi-Kobayashi Nov 27, 2025
275cec6
Implementation of the tab close button
Koichi-Kobayashi Nov 27, 2025
4349491
Optimization
Koichi-Kobayashi Nov 27, 2025
1a6422b
Add New Tab
Koichi-Kobayashi Nov 28, 2025
3936a0f
Modified the Add button for better visibility
Koichi-Kobayashi Nov 28, 2025
ca56de2
Optimization
Koichi-Kobayashi Nov 28, 2025
9c93cec
Visualization of Tab Movement
Koichi-Kobayashi Nov 29, 2025
5ccf68f
feat: Add TabAdding and TabClosing attached properties to TabControl
Koichi-Kobayashi Dec 6, 2025
c4678b9
Space Correction
Koichi-Kobayashi Dec 6, 2025
b0d0aca
Removal of unnecessary processing and correction of typos
Koichi-Kobayashi Dec 6, 2025
10a0662
Correction of typos
Koichi-Kobayashi Dec 6, 2025
8cf90d0
fix(controls): CardExpander has inconsistent CornerRadius (#1577)
maihcx Nov 24, 2025
468a3a7
chore: Update DocFX config (#1576)
pomianowski Nov 24, 2025
2abb361
fix(controls): Check if system accent matches colorization color (#1583)
Nuklon Nov 26, 2025
2cc8b04
fix(controls): enable access key support for multiple WPFUI controls …
apachezy Dec 9, 2025
7075d7d
fix(controls): Fix null binding error in scroll bar buttons when dele…
Koichi-Kobayashi Dec 9, 2025
08d4b3e
Merge branch 'main' into TabControl
Koichi-Kobayashi Dec 10, 2025
7bc8c9e
Merge branch 'main' into TabControl
Koichi-Kobayashi Jan 14, 2026
104126c
Fix TabControl close button visibility and spacing
Koichi-Kobayashi Jan 14, 2026
f78550f
Improved UI by removing the border from the extra + button
Koichi-Kobayashi Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,164 @@
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using Wpf.Ui.Controls;

namespace Wpf.Ui.Gallery.ViewModels.Pages.Navigation;

public partial class TabControlViewModel : ViewModel;
public partial class TabControlViewModel : ViewModel
{
[ObservableProperty]
private TabItem? _selectedTab;

// Stores the tab being dragged during drag-and-drop operation
private TabItem? _draggedTab;

// Stores the starting point of the drag operation
private Point _dragStartPoint;

// Indicates whether a drag operation is currently in progress
private bool _isDragging;

partial void OnSelectedTabChanged(TabItem? value)
{
// Update IsSelected property only for tabs that need to change
foreach (TabItem tab in StandardTabs)
{
bool shouldBeSelected = tab == value;
if (tab.IsSelected != shouldBeSelected)
{
tab.SetCurrentValue(TabItem.IsSelectedProperty, shouldBeSelected);
}
}
}

[ObservableProperty]
private ObservableCollection<TabItem> _standardTabs =
[
new TabItem
{
Header = CreateTabHeader("Hello", SymbolRegular.XboxConsole24),
Content = new System.Windows.Controls.TextBlock { Text = "World", Margin = new System.Windows.Thickness(12) },
IsSelected = true
},
new TabItem
{
Header = CreateTabHeader("The cake", SymbolRegular.StoreMicrosoft16),
Content = new System.Windows.Controls.TextBlock { Text = "Is a lie.", Margin = new System.Windows.Thickness(12) }
},
];

/// <summary>
/// Selects the specified tab and prepares for potential drag operation.
/// </summary>
[RelayCommand]
private void SelectTabForDrag(object? parameter)
{
if (parameter is not TabItem tabItem)
{
return;
}

// Select the clicked tab
SelectedTab = tabItem;
_draggedTab = tabItem;
}

/// <summary>
/// Starts the drag operation if the mouse has moved far enough.
/// </summary>
/// <param name="currentPoint">The current mouse position.</param>
/// <returns>True if drag operation was started, false otherwise.</returns>
public bool TryStartDrag(Point currentPoint)
{
if (_draggedTab == null || _isDragging)
{
return false;
}

// Check if the mouse has moved far enough to initiate a drag operation
double deltaX = currentPoint.X - _dragStartPoint.X;
double deltaY = currentPoint.Y - _dragStartPoint.Y;
double minDistance = SystemParameters.MinimumHorizontalDragDistance;

if (Math.Abs(deltaX) > minDistance || Math.Abs(deltaY) > minDistance)
{
_isDragging = true;
return true;
}

return false;
}

/// <summary>
/// Gets the tab being dragged.
/// </summary>
public TabItem? GetDraggedTab() => _draggedTab;

/// <summary>
/// Sets the starting point for drag operation.
/// </summary>
public void SetDragStartPoint(Point point)
{
_dragStartPoint = point;
}

/// <summary>
/// Ends the drag operation.
/// </summary>
public void EndDrag()
{
_draggedTab = null;
_isDragging = false;
}

/// <summary>
/// Reorders tabs by moving a tab from one position to another.
/// </summary>
/// <param name="draggedTab">The tab being moved.</param>
/// <param name="targetTab">The target tab position.</param>
public void ReorderTabs(TabItem draggedTab, TabItem targetTab)
{
if (draggedTab == targetTab)
{
return;
}

int draggedIndex = StandardTabs.IndexOf(draggedTab);
int targetIndex = StandardTabs.IndexOf(targetTab);

// Early return if indices are invalid or the same
if (draggedIndex < 0 || targetIndex < 0 || draggedIndex == targetIndex)
{
return;
}

StandardTabs.Move(draggedIndex, targetIndex);
SelectedTab = draggedTab;
}

private static System.Windows.Controls.StackPanel CreateTabHeader(string text, SymbolRegular symbol)
{
return new System.Windows.Controls.StackPanel
{
Orientation = System.Windows.Controls.Orientation.Horizontal,
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
Children =
{
new SymbolIcon
{
Symbol = symbol,
Margin = new System.Windows.Thickness(0, 0, 6, 0)
},
new System.Windows.Controls.TextBlock
{
Text = text
}
}
};
}
}
38 changes: 14 additions & 24 deletions src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wpf.Ui.Gallery.Views.Pages.Navigation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:uiControls="clr-namespace:Wpf.Ui.Controls;assembly=Wpf.Ui"
Title="TabControlPage"
d:DataContext="{d:DesignInstance local:TabControlPage,
IsDesignTimeCreatable=False}"
Expand All @@ -23,29 +23,19 @@
Margin="0"
HeaderText="Standard TabControl."
XamlCode="&lt;TabControl /&gt;">
<TabControl Margin="0,8,0,0">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Margin="0,0,6,0" Symbol="XboxConsole24" />
<TextBlock Text="Hello" />
</StackPanel>
</TabItem.Header>
<Grid>
<TextBlock Margin="12" Text="World" />
</Grid>
</TabItem>
<TabItem IsSelected="True">
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Margin="0,0,6,0" Symbol="StoreMicrosoft16" />
<TextBlock Text="The cake" />
</StackPanel>
</TabItem.Header>
<Grid>
<TextBlock Margin="12" Text="Is a lie." />
</Grid>
</TabItem>
<TabControl
Margin="0,8,0,0"
uiControls:TabControlExtensions.CanAddTabs="True"
uiControls:TabControlExtensions.CanReorderTabs="True"
uiControls:TabControlExtensions.TabAdding="OnTabAdding"
uiControls:TabControlExtensions.TabClosing="OnTabClosing"
ItemsSource="{Binding StandardTabs}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}">
<TabControl.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="TabItem">
<Setter Property="uiControls:TabControlExtensions.IsClosable" Value="True" />
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</controls:ControlExample>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// This Source Code Form is subject to the terms of the MIT License.
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.

using System.Windows.Controls;
using Wpf.Ui.Controls;
using Wpf.Ui.Gallery.ControlsLookup;
using Wpf.Ui.Gallery.ViewModels.Pages.Navigation;
Expand All @@ -14,11 +15,104 @@ public partial class TabControlPage : INavigableView<TabControlViewModel>
{
public TabControlViewModel ViewModel { get; }

/// <summary>
/// Initializes a new instance of the <see cref="TabControlPage"/> class.
/// The sample ensures the first tab remains selected and prevents it from being closed.
/// </summary>
public TabControlPage(TabControlViewModel viewModel)
{
ViewModel = viewModel;
DataContext = this;
DataContext = viewModel;

InitializeComponent();

// Ensure the first tab is selected and its content is displayed
if (ViewModel.StandardTabs.Count > 0)
{
ViewModel.SelectedTab = ViewModel.StandardTabs[0];

// Make the first tab non-closable
TabControlExtensions.SetIsClosable(ViewModel.StandardTabs[0], false);
}
}

/// <summary>
/// Handles the TabClosing event.
/// This event is raised when a user attempts to close a tab.
/// You can cancel the operation by setting e.Cancel = true.
/// </summary>
/// <remarks>
/// Examples of usage:
/// <list type="bullet">
/// <item>Prevent closing the last tab (implemented in this method).</item>
/// <item>Show confirmation dialog before closing (commented out, can be uncommented if needed).</item>
/// </list>
/// The tab will be automatically removed from ItemsSource by OnTabCloseRequested.
/// ViewModel's CloseTabCommand is not needed here as the removal is handled automatically.
/// </remarks>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments containing the tab item to close.</param>
private void OnTabClosing(object sender, TabClosingEventArgs e)
{
if (ViewModel.StandardTabs.Count <= 1)
{
e.Cancel = true;
return;
}
}

/// <summary>
/// Handles the TabAdding event.
/// This event is raised when a user clicks the add button to create a new tab.
/// You can customize the new tab by setting e.Header, e.Content, or e.TabItem.
/// You can cancel the operation by setting e.Cancel = true.
/// </summary>
/// <remarks>
/// This implementation uses Method 1: Setting tab properties using TabAddingEventArgs.
/// <list type="number">
/// <item>Get the tab number from the current tab count.</item>
/// <item>Set the header with an icon using CreateTabHeader.</item>
/// <item>Set the content to a TextBlock with the tab number.</item>
/// </list>
/// Alternative Method 2: You can also create a custom TabItem and assign it to e.TabItem for more control over tab creation.
/// Example: Cancel adding if maximum tabs reached (commented out, can be uncommented if needed).
/// </remarks>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments used to customize the new tab.</param>
private void OnTabAdding(object sender, TabAddingEventArgs e)
{
int tabNumber = ViewModel.StandardTabs.Count + 1;

e.Header = CreateTabHeader($"New Tab {tabNumber}", SymbolRegular.Document24);

e.Content = new System.Windows.Controls.TextBlock
{
Text = $"New Tab {tabNumber} content",
Margin = new System.Windows.Thickness(12)
};
}

/// <summary>
/// Creates a tab header with an icon and text.
/// </summary>
private static System.Windows.Controls.StackPanel CreateTabHeader(string text, SymbolRegular symbol)
{
return new System.Windows.Controls.StackPanel
{
Orientation = System.Windows.Controls.Orientation.Horizontal,
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
Children =
{
new SymbolIcon
{
Symbol = symbol,
Margin = new System.Windows.Thickness(0, 0, 6, 0)
},
new System.Windows.Controls.TextBlock
{
Text = text
}
}
};
}
}
2 changes: 1 addition & 1 deletion src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
/// <summary>
/// The maximum value of the background HSV brightness after which the text on the accent will be turned dark.
/// </summary>
private const double BackgroundBrightnessThresholdValue = 80d;

Check warning on line 55 in src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs

View workflow job for this annotation

GitHub Actions / build


/// <summary>
/// Gets the SystemAccentColor.
Expand Down Expand Up @@ -356,7 +356,7 @@
UiApplication.Current.Resources["AccentFillColorSelectedTextBackgroundBrush"] =
systemAccent.ToBrush();

var themeAccent = applicationTheme == ApplicationTheme.Dark ? secondaryAccent : primaryAccent;
Color themeAccent = applicationTheme == ApplicationTheme.Dark ? secondaryAccent : primaryAccent;
UiApplication.Current.Resources["AccentFillColorDefault"] = themeAccent;
UiApplication.Current.Resources["AccentFillColorDefaultBrush"] = themeAccent.ToBrush();
UiApplication.Current.Resources["AccentFillColorSecondary"] = Color.FromArgb(
Expand Down
45 changes: 45 additions & 0 deletions src/Wpf.Ui/Controls/TabControl/TabAddingEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.

using System.Windows;
using System.Windows.Controls;

// ReSharper disable once CheckNamespace
namespace Wpf.Ui.Controls;

/// <summary>
/// Provides data for the <see cref="TabControlExtensions.TabAddingEvent"/> event.
/// </summary>
public class TabAddingEventArgs : RoutedEventArgs
{
/// <summary>
/// Gets or sets the tab item to be added. If null, a new TabItem will be created.
/// </summary>
public TabItem? TabItem { get; set; }

/// <summary>
/// Gets or sets the content for the new tab.
/// </summary>
public object? Content { get; set; }

/// <summary>
/// Gets or sets the header for the new tab.
/// </summary>
public object? Header { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the add operation should be canceled.
/// </summary>
public bool Cancel { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="TabAddingEventArgs"/> class.
/// </summary>
public TabAddingEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
}

Loading
Loading